Get Started

Comparing cli-plus to @feathersjs/cli

@feathers-plus/cli, a.k.a. "cli-plus", is similar to @feathersjs/cli in that:

  • It uses the same commends, e.g. generate service.
  • It prompts with the same questions, e.g. "Which path should the service be registered on?"
  • It generates the same modules with essentially identical code.

However cli-plus also:

  • Converts DB-agnostic JSON-schema models to the DB model each service is currently configured for.
  • Generates quality fake data and optionally seeds your DB with it.
  • Generates unit and client/server tests for your hooks, services and authentication.
  • Generates all the code needed for your GraphQL Query interface.
  • Generates either JavaScript or TypeScript code; with semicolons or without. Converts apps between JS and TS.
  • Regenerates you app without losing changes you've made. You stay up-to-date with enhancements and fixes.

Installation

npm i -g @feathers-plus/cli

What the Generator Does

Generators help eliminate boilerplate.

Feathers, even when coded "by hand", eliminates the majority of the boilerplate typically in a CRUD project. Generators will eliminate even more.

Generators.

Feathers generators produce very little code because Feathers is so succinct. You can easily understand the generated code because it's no different from what we've been coding "by hand" so far. Some other frameworks make things “seem” easy by generating thousands of lines of code for you and, in the process, making it almost impossible to implement anything not supported out of the box by their generators.

Generators structure your app.

The generated modules are structured as recommended by the Feathers team.

Generators handle database specifics.

The generators will generate code for different databases so you don't have to investigate how to do so.

Switching between adapters.

You can change a service's database adapter while regenerating it. This allows you, for example, to quickly mock up an app using the NeDB adapter, and then convert it to Mongoose.

Use REST and GraphQL together

REST and GraphQL are totally different. Each is dope if used for the right thing.

cli-plus allows us to specify our services (a REST concept) along with their models and relations. It then generates an additional GraphQL endpoint.

REST vs GraphQL

FeathersJS, using cli-plus, now supports both REST and GraphQL architectural concepts and their query languages.

Windows 10

The Yeoman toolkit used by cli-plus has an outstanding issue when run on Windows 10 with Node 10. You either have to call cli-plus from Git Bash using either Node 8 or 10, or you have to use Node 8 when calling cli-plus from PowerShell or command.exe. The generated code will run on Node 10.

generate options

Let's generate a project having users who may be members of one or more teams. We want to display teams with all their members.

Database schema for project.

vTitle


We first create a folder to contain the app, and then set its generator options.

mkdir feathers-app
cd feathers-app
feathers-plus generate options

Prompts

Prompts 'generate options'

vTitle


  • Source scan took 0s 23ms. The generator scans the app to identify any custom code. This will always be very fast, taking much less time than what the Yeoman tool needs to start up.

  • The generator will not change the following modules. The generator can refresh many of the modules in your app, updating them to the latest version of the generator. It does this without changing any of your custom code.

The listed modules are ones which the generator will not update. Once they are written, the generator will not refresh them. Any changes you make will remain. If you delete any of them, the generator will replace them.

This list is displayed as a convenience, so you can refer to it anytime by running generate options and cancelling.

  • You have additionally prevented the following modules from being changed. You can prevent a.k.a. "freeze" modules to prevent the generator from refreshing them. You normally would not do so but it's useful to have the option. The frozen modules are listed here for convenience.

  • Generate TypeScript code? Defaults to no. Otherwise JavaScript code is generated.

  • Use semicolons? Defaults to yes. Otherwise semicolons are not generated at the end of statements.

  • View module changes and control replacement (not recommended)? Defaults to no. This is a Yeoman feature which allows you to inspect the changes to be made to any module, and to decide whether to apply it. Its useful when you are exploring the raw details of what the generator writes.

Folder

The feathers-app folder now contains one module.

Folders after 'generate options'

vTitle


  • feathers-gen-specs.json contains the information used to generate the app.

feathers-gen-specs.json

{
  "options": {
    "ver": "1.0.0",
    "inspectConflicts": false,
    "semicolons": true,
    "freeze": [],
    "ts": false
  },
  "app": {},
  "services": {}
}

The generator keeps the prompts used to generate the app in feathers-gen-specs.json. It can regenerate your entire app from that and the custom code it finds in the modules.

Regeneration.

The generator will default to your previous choices if you rerun feathers-plus generate options. It will make any needed changes to all modules if you change any of the options.

Optional

You need not run feathers-plus generate options if your app will be in JavaScript with semicolons.

generate app

Next we generate the scaffolding for the app.

feathers-plus generate app

Prompts

Prompts 'generate app'

vTitle


  • Project name. Defaults to the folder name.

  • Description. Optional description for the project.

  • What folder should the source files live in? Default src. The name of the folder containing the server code.

  • Which package manager are you using? Default npm. The alternative is yarn.

  • What types of API are you making? The options are

    • REST - HTTP. Default.
    • Realtime via Socket.io - Default.
    • Realtime via Primus
  • Data mutating tests and seeding may run when NODE_ENV is one of (optional).

A Feathers app runs in an environment set by the NODE_ENV environment variable. You may decide on having different environments such as the default (NODE_ENV not specified), development, production, staging, and uat (user acceptance testing).

Some of the modules generated by cli+ are capable of changing the services' data. They can seed the services' data on startup with fake records generated by cli+ (see generate fakes). Or they can create, change and remove records during tests.

We want to be very careful not to accidentally change, for example, the production records. So enter here the comma-separated environment names during which the service's data may be changed by seeding or tests. The name test would be a reasonable choice.

The generated seeding and test mutating code will only run in this environment, skipping processing in other environments. This prevents accidental changes to your data.

NODE_ENV.

None of this mutating code will run if NODE_ENV is not specified on the Node command line, or if its specified but not given a value.

You can remove all current environment names by typing a space at the prompt.

  • Seed service records on startup when command line includes --seed?.

Code to seed the services data on startup is generated only if yes is selected. This code will only run in one of the NODE_ENV environments selected in the previous prompt and only if the node command line contains a --seed argument. An example of such a command would be NODE_ENV=test node src/ --seed.

More information

This article explains how the generated code handles the securing of the application based on NODE_ENV.

Folders

The generator creates some JS modules or TS ones reflecting your choices. The modules are properly wired together and structured as recommended by the Feathers team.

Folders after 'generate app' with JavaScript

vTitle


Folders after 'generate app' with TypeScript

vTitle


  • config/ contains the configuration files for the app. production.json values override default.json ones when in production mode, i.e. when you run NODE_ENV=production node path/to/your/server.js.

  • config/test.json was created because we specified test as the environment where we can seed and change data during testing. test.json initially contains

{
  "mongodb": " ",
  "mysql": " ",
  "nedb": " ",
  "postgres": " ",
  "rethinkdb": " ",
  "sqlite": " ",
  "mssql": " "
}

This overrides the database connection strings in default.json and prevents unwanted data changes.

  • node_modules/. The generator installs the project dependencies here using either npm, or yarn if that's installed. The dependencies are enumerated in package.json.

  • public/ contains the resources to be served. A sample favicon and HTML file are included.

  • src/ contains the Feathers server.

    • hooks/ contains your custom hooks, usually those general enough to be used with multiple services. A simple but useful logger is provided as an example.

    • middleware/ contains your Express middleware.

    • refs contains definitions for fields which may reside in multiple database models.

    • services/ will contain the services.

    • index.?s is used by node to start the app.

    • app.?s configures Feathers and Express.

    • app.hooks.?s contains hooks which are run for all services.

    • channels.?s contains the channels sending realtime events to clients.

    • logger.?s configures the Winston logger.

    • seed-data.?s seeds data when the app starts.

    • app.interface.ts contains the global App interface needed to provide typed services.

    • typings.d.ts contains declarations of modules that come without typings.

  • test/ contains the tests for the app. app.test.?s tests that the index page appears, as well as 404 errors for HTML pages and JSON.

  • .editorconfig is compatible with the EditorConfig project and helps developers define and maintain consistent coding styles among different editors and IDEs.

  • .gitignore specifies intentionally untracked files which git, GitHub and other similar projects ignore.

  • feathers-gen-specs.json contains the information used to generate the app.

  • LICENSE contains the license so that people know how they are permitted to use it, and any restrictions you're placing on it. It defaults to the Feathers license.

  • package.json contains information which npm, yarn and other package managers need to install and use your package.

  • package-lock.json is created by npm. It contains the versions of the module dependencies which are installed. It exists only if the npm option is selected.

  • README.md contains the documentation for the app.

  • tsconfig.json contains the TypeScript compiler's configuration used for building the app.

  • tsconfig.test.json contains the TypeScript compiler's configuration used for building tests.

  • tslint.json contains tslint configuration.

  • yarn.lock is created by yarn. It contains the versions of the module dependencies which are installed. It exists only if the yarn option is selected.

Adding your code

@feathersjs/cli's job ends when it generates the app scaffolding. It doesn't know what you do afterwards with it.

Cli-plus is a round-trip generator. Round-trip generators can take previously generated code, identify custom changes made to it, and regenerate the code (maybe using different responses to the prompts) along with those custom changes.

Cli-plus completes the round trip: generate -> customize -> regenerate -> customize -> ....

The developer and cli-plus are in a more collaborative relationship. They can work co-operatively on the scaffolding code.

Retain developer modifications

You will usually add your own code to the generated modules. Cli-plus can identify such additional code, as long as certain standards are followed, and it will retain that added code when regenerating modules.

Some of the code generated by cli-plus is identified as default code which you may want to customize. Any customized code replacing the default code is also retained when modules are regenerated.

Let's look at a trivial example of these features. @feathersjs/cli and cli-plus both create module src/index.js when generate app is run. The cli-plus module has some extra decorative comments:

server.on('listening', () => {
  // !<DEFAULT> code: listening_log
  logger.info('Feathers application started on http://%s:%d', app.get('host'), port);
  // !end
});

// !code: end // !end

Starting the server produces the expected log:

Feathers application started on http://localhost:3030

The lines between // !<DEFAULT> code: listening_log and // !end contain default code named listening_log.

The // !code: end // !end line identifies a location named end where additional lines may be added.

Let's change the code to:

server.on('listening', () => {
  // !code: listening_log <-- Note that <DEFAULT> was removed.
  logger.info('Hello world on http://%s:%d', app.get('host'), port);
  // !end
});

// !code: end
logger.info('Initialization complete. Waiting for server to start.'); 
// !end

Starting the server now logs:

Initialization complete. Waiting for server to start.
Hello world on http://localhost:3030

A common error

Notice the <DEFAULT> tag was removed from listening_log along with changing the default code itself. Any code changes you make in location_log will be lost on regeneration if you don't remove that <DEFAULT>.

Let's say you originally ran generate app selecting only socket.io as a transport. You then changed the code in src/index.js as described above. Later on you realize you also need REST as a transport. You can just rerun generate app with cli-plus and select both transports. The regenerated modules will contain the code changes you made above.

Where can code be added?

The short answer is "just about anywhere". Insertion points are available anywhere it makes any sense to add code.

Here is a typical src/services/index.js:

// Configure the Feathers services. (Can be re-generated.)
let comment = require('./comment/comment.service');
let like = require('./like/like.service');
let post = require('./post/post.service');
let relationship = require('./relationship/relationship.service');
let user = require('./user/user.service');

let graphql = require('./graphql/graphql.service');
// !code: imports // !end
// !code: init // !end

let moduleExports = function (app) { // eslint-disable-line no-unused-vars
  app.configure(comment);
  app.configure(like);
  app.configure(post);
  app.configure(relationship);
  app.configure(user);

  app.configure(graphql);
  // !code: func_return // !end
};

// !code: exports // !end
module.exports = moduleExports;

// !code: funcs // !end
// !code: end // !end

All the modules follow the same standards, e.g. starting with imports and init, and ending with funcs and end. Functions tend to end with names like func_return so you can add code to them. You can replace or mutate the required modules at init as well as adding any initialization code. You can modify or replace the exported value at exports.

Its trivial to add insertion points into the generator, so create an issue if you need additional ones. We'll add that insertion point, and you just regenerate the app to be able to use it.

More realistic code customization

The previous example gets the idea across but it's too trivial to appreciate the impact of the feature.

Cli-plus generates a module for every service named src/services/serviceName/serviceName. validate.js. It contains JSON-schema which may be used to validate record contents before create, update and patch calls. Part of the code may be similar to:

const base = merge({},
  // !<DEFAULT> code: base
  {
    $schema: "http://json-schema.org/draft-05/schema",
    title: "User",
    description: "User database.",
    required: [
      "uuid",
      "email",
      "firstName",
      "lastName"
    ],
    properties: {
      _id: {
        type: ID
      },
      uuid: {
        type: ID
      },
      email: {
        type: "string"
      },
      firstName: {
        type: "string", maxLength: 30,
      },
      lastName: {
        type: "string", maxLength: 30,
      }
    }
  },
  // !end
  // !code: base_more // !end
);

This 'base' JSON-schema is derived from the service model. (The service model is a new concept cli-plus introduces which is explained below.) It is the basis for the validation schemas used for the different types of calls.

The developer may replace the default code named base to suit his use case.

It would probably be better however to mutate the default schema by adding, mutation or removing properties by adding custom code to base_more, e.g.:

// !code: base_more
{
  properties: {
    firstName: { minLength: 45 },
    initial: { type: 'string', maxLength: 1 }
  }
}
// !end

This would be better because now, when you change the service model, cli-plus will make appropriate changes to the base schema in its default code. This is better than remembering to modify the base schema manually every time you change the service model.

GraphQL examples

One of the main features of cli-plus is its ability to generate a GraphQL endpoint as well as the necessary resolvers. A large number of carefully coded resolvers need to defined in a reasonably sized project, so automatically generating these resolvers is a valuable feature.

However resolvers often have to be customized in unexpected ways. You may need to change the sort order. You may need to set props in context.params for certain hooks. There is no practical end to the potential customizations.

Here are some code snippets in src/services/graphql/service.resolvers.js which cli-plus may generate for a GraphQL endpoint:

You can customize them as you wish, by defining a $sort order for example.

fullname is a calculated field. Cli-plus, rather than inventing some specialized way for you to indicate what the calculation is, just creates some default code for you to replace with the calculation. For example

      // fullName: String!
      fullName:
        // !code: resolver-User-fullName-non
        (parent, args, content, ast) => `${parent.firstName} ${parent.lastName}`,
        // !end

Avoiding customization

You can have cli-plus generate modules and then prevent it from making changes thereafter to some of them, by using the options.freeze prop in feathers-gen-specs.json.

Some details

The leaders for custom code may be : // !code:, // !<> code:, //!code:, or // ! code:.

The trailers may be: // !end, or //!end

generate authentication

Let us next generate authentication for the app. This will generate

  • The authentication code itself.
  • A service for the users table.
  • A connection string for the database containing the users table.
feathers-plus generate authentication

Prompts

Prompts 'generate authentication'

vTitle


  • Authentication questions:

    • What authentication providers do you want to use? Choose one or more of: Local, Auth0, Google, Facebook and Github.
    • What is the name of the user (entity) service? Default users. The name of the users table.
  • Service questions:

    • What would you call one row in the <users> database? Our default is user. This response is used to form the names of GraphQL types e.g.User and [User!].
    • What kind of service is it? Default is NeDB. Choose one of: Custom service, Memory, NeDB, MongoDB, Mongoose, Sequelize, KnexJS.
    • Generate service in which nested folder, e.g. v1/blog? (optional) Default is none. Nested folders are explained in generate service.
    • Which path should the service be registered on? Default /users.
    • Should this be served by GraphQL? Default yes. Should this service be included in GraphQL Queries?
  • Connection question:

    • What is the database connection string? How do we attach to the database? The default is the standard connection string for that database.

Folders

The generator adds some modules to the JS folder or TS one.

Folders after 'generate authentication' with JavaScript

vTitle


Folders after 'generate authentication' with TypeScript

vTitle


  • models/. Some database adapters require models for their tables. Those models are located here.

    • users.model.?s is the model for the users table.
  • services/users contains code for the users service.

    • users.service/?s configures the service.
    • users.hooks.?s identifies the hooks to be used with the service.
    • users.schema.?s contains the database agnostic JSON-schema model for the service. When a service is initially created. the module contains an empty model as no model has yet been defined.
    • users.mongo.?s, users.mongoose.?s, users.sequelize.?s contain the JSON-schema model in users.schema.?s converted to a MongoDB, Mongoose or Sequelize model.
    • users.validate.?s contains schemas to validate data for create, patch and update service calls.
    • users.interface.ts contains a TypeScript interface describing a single user entity.
  • authentication.?s contains the authentication code.

  • mongoose.?s configures the Mongoose adapter. knex.?s, mongodb.?s, rethinkdb.?s, sequelize.?s and sequelize-mssql.?s models would also be created as needed.

  • test/service/users.test.?s tests the service can be started.

All those modules

Only one of users.mongo.?s, users.mongoose.?s, users.sequelize.?s is being used at any time. The code generated for users.service.?s and users.model.?s call the right modules for the What kind of service is it? selected.

The modules are all generated so you may include all the custom code you need for all of them. This allows you to rerun generate authentication or generate service at any time to change databases.

Feathers Models

Some database systems use a schema to describe how the data in a database table or collection is organized, as well as how the different schemas relate to one another. Unfortunately, schemas are normally not shareable between different databases. The Mongoose database adapter, for example, will not understand a schema written for the Sequelize database adapter.

However if you use Feathers Models, cli-plus can automatically convert your Feathers model into the schema expected by a particular database adapter.

With Feathers service adapters and Feathers Models you can connect to the most popular databases and query them with a unified interface no matter which one you use. This makes it easy to swap databases and use entirely different DBs in the same app without changing your application code.

JSON-schema

Feathers Models are based on JSON-schema. JSON-schema is the most popular way to describe the structure of JSON data and, since JSON data is essentially just plain old JavaScript objects, this makes JSON-schema a great fit for Feathers Models.

JSON-schema:

  • has the widest adoption among all standards for JSON validation.
  • is very mature (current version is 6).
  • covers a big part of validation scenarios.
  • uses easy-to-parse JSON documents for schemas.
  • is platform independent.
  • is easily extensible.
  • JSON-schema is easy to write, and there are some great tutorials.

The validateSchema common hook already uses JSON-data for verification.

Ecosystem

JSON-schema has a large ecosystem. The JSON-schema manual serves as an introduction to it.

Adding the Feathers Model

The users service was created with an empty Feathers model, i.e. it has no fields. Let's now define a model for it in module src/services/users/users.schema.?s. The guide contains detailed information on defining models.

Our resulting model contains 4 fields, and an id field will be added automatically.

// Define the model using JSON-schema
let schema = {
  // !<DEFAULT> code: schema_header
  title: 'Users',
  description: 'Users database.',
  // !end
  // !code: schema_definitions // !end

  // Required fields.
  required: [
    // !code: schema_required
    'email',
    'firstName',
    'lastName',
    'roleId'
    // !end
  ],
  // Fields with unique values.
  uniqueItemProperties: [
    // !code: schema_unique // !end
  ],

  // Fields in the model.
  properties: {
    // !code: schema_properties
    id: { type: 'ID' },
    email: {},
    firstName: {},
    lastName: {},
    password: {},
    roleId: { type: 'ID' }
    // !end
  },
  // !code: schema_more // !end
};
  • type: 'ID' indicates the field contain a key (id) or a foreign key (roleId), that is, the field contains the key to another record.

  • type: {} The default is 'string'.

id or _id

The generator does not change the name of index fields based on the type of adapter the service uses. You have to define a property _id: { type: 'ID' } field in model if your database id field is named _id.

Sequelize

Sequelize by default adds the fields createdAt and updatedAt to records. You need to define these 2 fields in the JSON-schema model as that model is DB agnostic. Define them as createdAt: { format: 'date-time' }.

Centralizing field definitions

Rather than defining the properties of the same type of field, e.g. description, in every model its defined in, you can define the field type once and refer to it from models using the $ref property.

This is an important concept in JSON-schema and its explained elsewhere.

Regenerating the Service

We can now regenerate the service. The generator will detect the Feathers model and use it to "fill in" modules such as users.mongoose.?s and users.validate.?s.

feathers-plus generate service
Prompts 'generate authentication' with model

vTitle


The responses used to create the users service are the defaults presented now. Just Enter through all the prompts.

The folder organization will not change, however all modules now reflect the Feathers model. src/services/users/users.mongoose.js is an example:

As is src/services/users.users.validate.js:

generate secret

generate authentication added a secret to config/default.json:

{
  "host": "localhost",
  "port": 3030,
  "public": "../public/",
  "paginate": {
    "default": 10,
    "max": 50
  },
  "authentication": {
    "secret": "1a3808d2bfd79fc3370b0bdcf76ee95b4f9833894041aeb4f0964b99a9a02929c6cdc64180c4a55a02fd6245a6ee462501c40def69d3984ce17f93967ea389918e09fad8ea1efeb7579f4515d66fd965a235604e5c356a6a71c4f999cba05d8ef56eee787364fe086741099bd83cbef21e983782d0d5032ea6d9475518b2521388df49a460b430d76494076772fe57bb9725be1c23dd9f704daba8b4c86340173314860cccec9bc39b25a90390ac5d60823737d6a3d5067a7a44994f22f2f7268b59732853084eccada87d66962fe8d239f16d1fa7c795500e23bd56dc8d64357f9fd33631649a3cba4b5ce50d8b47a7c4117ac84f2aed13157e1447e21bb99c",
    // ...
  },
  "mongodb": "mongodb://localhost:27017/feathers_app"
}

If you need to create a new secret, run

feathers-plus generate secret
Prompts 'generate secret'

vTitle


and paste the new secret into config/default.json.

generate service

Let's add 2 new services using:

feathers-plus generate service

First let's add a roles service.

Prompts 'generate service' for Roles

vTitle


And then a teams service.

Prompts 'generate service' for Teams

vTitle


Nested Folders

The Generate service in which nested folder, e.g. v1/blog? (optional) prompt allows you to group related services together. Their modules, models, tests and (optionally) service call paths will reflect the organization.

Services folder with and without nested folders

vTitle


Adding the Feathers Models

Now we add a Feathers model to src/services/roles/roles.schema.?s:

let schema = {
  // !<DEFAULT> code: schema_header
  title: 'Roles',
  description: 'Roles database.',
  // !end
  // !code: schema_definitions // !end

  // Required fields.
  required: [
    // !code: schema_required
    'name'
    // !end
  ],
  // Fields with unique values.
  uniqueItemProperties: [
    // !code: schema_unique // !end
  ],

  // Fields in the model.
  properties: {
    // !code: schema_properties
    id: { type: 'ID' },
    name: {}
    // !end
  },
  // !code: schema_more // !end
};

And one to src/services/teams/teams.schema.?s:

// Define the model using JSON-schema
let schema = {
  // !<DEFAULT> code: schema_header
  title: 'Teams',
  description: 'Teams database.',
  // !end
  // !code: schema_definitions // !end

  // Required fields.
  required: [
    // !code: schema_required
    'name',
    'members'
    // !end
  ],
  // Fields with unique values.
  uniqueItemProperties: [
    // !code: schema_unique // !end
  ],

  // Fields in the model.
  properties: {
    // !code: schema_properties
    id: { type: 'ID' },
    name: {},
    members: {
      type: 'array',
      items: { type: 'ID' }
    }
    // !end
  },
  // !code: schema_more // !end
};

Regenerating the Services

And regenerate, accepting the defaults by pressing Enter.

feathers-plus generate service
Prompts 'generate service' for Roles with Model

vTitle


Prompts 'generate service' for Teams with Model

vTitle


The Mongoose models now reflects the Feathers model:

/* eslint quotes: 0 */
// Defines Mongoose model for service `roles`. (Can be re-generated.)
const merge = require('lodash.merge');
// eslint-disable-next-line no-unused-vars
const mongoose = require('mongoose');
// !code: imports // !end
// !code: init // !end

let moduleExports = merge({},
  // !<DEFAULT> code: model
  {
    name: {
      type: String,
      required: true
    }
  },
  // !end
  // !code: moduleExports // !end
);

// !code: exports // !end
module.exports = moduleExports;

// !code: funcs // !end
// !code: end // !end
/* eslint quotes: 0 */
// Defines Mongoose model for service `teams`. (Can be re-generated.)
const merge = require('lodash.merge');
// eslint-disable-next-line no-unused-vars
const mongoose = require('mongoose');
// !code: imports // !end
// !code: init // !end

let moduleExports = merge({},
  // !<DEFAULT> code: model
  {
    name: {
      type: String,
      required: true
    },
    members: [
      String
    ]
  },
  // !end
  // !code: moduleExports // !end
);

// !code: exports // !end
module.exports = moduleExports;

// !code: funcs // !end
// !code: end // !end

Folders

The generator adds some modules to the JS folder or TS one.

Folders after 'generate service' with JavaScript

vTitle


Folders after 'generate service' with TypeScript

vTitle


  • models/teams.model.?s contains the model for teams.

  • services/teams contains modules for teams.

  • test/services/teams.test.?s tests the team service is configured.

generate fakes

So far we've generated the code for our application. However there isn't very much we can do with that code because the services have no data.

cli+ skirts this issue by providing comprehensive features for generating fake data based on your JSON-schema models. This includes generating values for foreign key fields which refer to other generated records.

userID: '507f1f77bcf86cd799439011'

cli+ is notable in that few other data generators can populate foreign key fields.

Generating appropriate fake data

We'd like the fake data to resemble what our real data would look like.

  • email address fields that look like valid addresses.
  • phone number fields that look like phone numbers tailored for a particular locale.
  • avatar references that point to real images.

We can describe the fake data we want by adding a faker, chance and format prop to each field's JSON-data.

  • fakeRecords: 6 - Generate 6 faked records for this service. The default, which you can change, is 5 records.
  • id or _id - The record's key field does not need a faking prop.

Key values appropriate for the service adapter

A key value will be generated that's suitable for that service's adapter. So MongoDB, Mongoose services use ObjectId strings, while Sequelize, Knex ones use increasing integers.

  • chance: { hash: { length: 60 } } - generates a 60 character hash value.
  • faker: { fk: 'roles:random' } - uses the key from a randomly generated record in the roles service.

A wealth of options

There are about 150 different types of faker props avaliable. They can be localized to about 35 locales so, for example, your fake data can be in Spanish or German.

There are also over 100 chance props available.

Custom props

You can also add your own custom faker, chance and format props.

Further customizing the generated data

The generated fake data will also satisfy the other JSON-schema specified for the field. So the more detailed your JSON-schema, the more the generated data will resemble what you expect in production.

It's a win, win.

Not only will a more detailed JSON-schema produce better generated data, it will also produce better validation checking in the automatically generated validation service hooks.

Prompts

Now that we've described what the fake data should look like, we can generate it.

feathers-plus generate fakes
Prompts 'generate fakes'

vTitle


Folders

The generator adds two modules to the app.

Folders after 'generate fakes' with JavaScript

vTitle


  • seeds/fake-data.json. The generated fake data.
  • config/defaults.js. Contains the configuration used to generate the fake data. You can change any of its options and rerun feathers-plus generate fakes.

Foreign keys

cli+ also supports foreign keys. We previously saw

    roleId: {
      type: 'ID',
      faker: { fk: 'roles:random' },  
    },

faker: { fk: 'roles:random' } is a custom prop defined by cli+. It populates the field with the key from a randomly selected record from the roles service.

Circular foreign keys

There are no restrictions on the foreign keys. Services A and B may each have a foreign key referencing the other. Service A may reference service B, which references service C, which then references back to service A.

The following syntax is supported:

  • 'roles:random:fieldName' A random record is selected from the roles service. The value of its fieldName field is used.
  • 'roles:random' - The value of the id or _id field is used.
  • 'roles' - The same as 'roles:random'.

TIP

fieldName may be in dot notation, e.g. "address.city".

Arrays of distinct foreign keys

The above syntax alone cannot handle several needs we may have. First, we may need an array of distinct foreign keys:

The memberIds field may contain duplicate foreign keys if we use faker: { fk: 'users' } or faker: { fk: 'users:random' }.

The list of keys for the roles service is shuffled at the start of each teams record. The faker: { fk: 'users:next' } will use the next one of these shuffled keys. Keys will start to be reused only once all existing keys have been used.

This ensures the foreign keys in memberIds are distinct.

Arrays

The syntax for items is very specific, if you give an array of schemas you'll get them as result, one-by-one, e.g. { items: [{ type: 'string' }] }.

If you give an object instead, then the given schema will be used for all the generated items, e.g. { items: { type: 'string' } }.

We may need to copy the values of several fields from the same related record. For example we may need a result like

memberIds: [{ id, firstName }, { id, firstName }, ... ]

We can request this as follows

'users:next' selects the record containing the next shuffled key. Then 'users:curr:firstName' refers to this current shuffled record, that is, the same one which provided the key.

You can extract any number of fields from a related record using this technique.

Expressions

  • Sometimes an expression has to be calculated in order to populate a field.






 




  properties: {
    // !code: schema_properties
    id: { type: 'ID' },
    // ...
    dayOfWeek: {
      type: 'integer',
      faker: { exp: 'new Date("December 25, 2018").getDay()'}
    }
    // !end
  },

faker: { exp: 'new Date("December 25, 2018").getDay()'} is a custom prop defined by cli+. It populates the field with the calculated day of the week.

  • You can reference other fields in the record within the expression.









 




  properties: {
    // !code: schema_properties
    id: { type: 'ID' },
    // ...
    date: {
      faker: 'date.soon', // e.g. "2018-12-25T04:49:43.661Z"
    },
    dayOfWeek: {
      type: 'integer',
      faker: { exp: 'new Date(rec.date).getDay()' }
    }
    // !end
  },

rec is an object passed to the expression containing the record being populated. You can use it in any valid JavaScript expression.

  • Other conveniences are also passed to the expression in the ctx object.
    • ctx.data (Array< Object >) - All the generated records for all the services. Values for foreign keys and expressions have been calculated up to the current record.
    • ctx.dataCurrIndex (Number) - Offset of the current record in ctx.data[serviceName].
    • ctx.hashPassword (Function(password)) - Calculates a password hash compatible with Feathers' hashPassword hook. This password will work correctly with Feathers local authentication.
    • fakeData.expContext from module config/default.js is also merged into ctx. You can refer to any of those merged properties in the expression.
    • ctx.tablesInfo {Object) - A structure you can use to determine the current shuffled key for each service. Its structure is:

Passwords

You can generate password hashes two ways

  • faker: { exp: 'ctx.hashPassword("my secret password")' } generates a Feathers compatible password you can use with local authentication. Its slow, taking over 1 second per password.
  • chance: { hash: { length: 60 } } quickly generates a random hash the same length as Feathers'. Its not compatible with Feathers' local authentication.

Customizing data generation

You can define your own custom props for faker, chance and format. They are configured in config/default.js and that module contains documentation on how to define and use them.

You can also modify the generated data before it is written to seeds/fakeData.json with the postGeneration property in config/default.js.

More information

This article contains more information about generating fake data.

GraphQL

There is a good deal of literature available on what GraphQL is, on why it is good, and how it compares to REST.

Some take-aways are:

  • REST and GraphQL are totally different.
  • GraphQL isn't a magic bullet, nor is it better.
  • You can definitely use both at the same time.
  • GraphQL is dope if used for the right thing.

cli-plus automatically generates a GraphQL endpoint for your Feathers services, so you may make queries using either REST or GraphQL, even simultaneously.

Our app as an example

We've taken the app we've been working on and produced a GraphQL example . You can run it yourself with npm i and npm start.

  • We converted the services to NeDB so the example is easy to run.
  • We added some additional information to the service schemas.
  • We initialize the NeDB tables in src/index.?s.
  • We also run some GraphQL queries there.

Types

STOP

Don't run this command right now. Its shown here for illustrative purposes.

When you run

feathers-plus generate graphql

GraphQL types are generated, along with the fields on those types. The types generated for our example are:

type Role {
  id: ID
  name: String!
}
 
type Team {
  id: ID
  name: String!
  memberIds: [String]
  members: [User!]
}
 
type User {
  id: ID
  email: String!
  firstName: String!
  lastName: String!
  password: String
  roleId: ID!
  fullName: String!
  role(query: JSON, params: JSON, key: JSON): Role
  teams(query: JSON, params: JSON, key: JSON): [Team!]
}
 

type Query {
  getRole(key: JSON, query: JSON, params: JSON): Role
  findRole(query: JSON, params: JSON): [Role]!
  getTeam(key: JSON, query: JSON, params: JSON): Team
  findTeam(query: JSON, params: JSON): [Team]!
  getUser(key: JSON, query: JSON, params: JSON): User
  findUser(query: JSON, params: JSON): [User]!
}

The getRole, findRole, etc. are the GraphQL queries you can make. The other type expand what the queries can return.

Notice the API for the types is the Feathers service API by default.

  • key is the Feathers id,
  • query is Feathers params.query,
  • params is anything else in Feathers params.

Using the same API reduces what you have to learn, and makes the interface between Feathers and GraphQL seamless.

JSON

You may be perplexed by the JSON type used above as it's not one of GraphQL's scalar types. It is a custom type added by @feathers-plus/graphql.

In GraphQL notation

  • String is a string or null.
  • String! is a string.
  • [String] is an array of strings, some of which may be null. It may also be null instead of an array.
  • [String]! is similar to [String] but it cannot be null.
  • [User!] is an array of User objects. None of the elements may be null.

Making our first request

The client (or the server) can then make a request like

const graphql = app.service('graphql');
const myQuery = '{
  findUser(query: {}) {
    firstName
    lastName
    fullName
    email
    role { name }
    teams { name }
  }
}';
                  
const result = await graphql.find({ query: { query: myQuery } })

GraphiQL

Why the two query in { query: { query: myQuery } }? The first is needed by the Feathers service API of course. The second one is necessary in order to optionally use GraphiQL, the popular graphical interactive in-browser GraphQL IDE. You can use GraphiQL to test your queries from the browser.

Darn that `$`

$ is a reserved char in GraphQL, so the following call will not behave as expected

graphql.find({ query: { query: 'findUser(query: { $sort: { lastName: 1 } })' } })

@feathers-plus/graphql's workaround is to code double underscores __ instead of a $. Instead of the above, we would use

graphql.find({ query: { query: 'findUser(query: { __sort: { lastName: 1 } })' } })

In short, replace every $ you would use in Feathers with __ in the GraphQL request.

The request returns data of the form:

{
  total: 3,
  limit: 10,
  skip: 0,
  data: {
    findUser: [
      {
        firstName: 'Jack',
        lastName: 'Doe',
        fullName: 'Jack Doe',
        email: 'jack-doe@gmail.com',
        role: { name: 'normal' },
        teams: [ { name: 'beta' } ]
      }, {
        firstName: 'Jane',
        lastName: 'Doe',
        fullName: 'Jane Doe',
        email: 'jane-doe@gmail.com',
        role: { name: 'normal' },
        teams: [ { name: 'alpha' }, { name: 'beta' } ]
      }, {
        firstName: 'John',
        lastName: 'Doe',
        fullName: 'John Doe',
        email: 'john-doe@gmail.com',
        role: { name: 'admin' },
        teams: [ { name: 'alpha' } ]
      }
    ]
  }
}

Some of the advantages of GraphQL are becoming clear:

  • The complete response was returned in a single request.
  • The client asks only for the data it needs.
  • The client does not know, nor does it care how the data is organized, or what needs to be done to accumulate the complete response.
  • No versioning exists.

Making another request

And a request like:

{
  findTeam(query: {}) {
    name
    memberIds
    members {
      fullName
      role { name }
    }
  }
}

returns

{
  total: 2,
  limit: 10,
  skip: 0,
  data: {
    findTeam: [
      {
        name: 'alpha',
        memberIds: [ 'gXWQs77x3X1fZjVg', 'hVJjFMnxCfHnIBS0' ],
        members: [
          { fullName: 'Jane Doe', role: { name: 'normal' } },
          { fullName: 'John Doe', role: { name: 'admin' } }
        ]
      }, {
        name: 'beta',
        memberIds: [ '6bJA2ONlIEpA8IX8', 'hVJjFMnxCfHnIBS0' ],
        members: [
          { fullName: 'Jack Doe', role: { name: 'normal' } },
          { fullName: 'Jane Doe', role: { name: 'normal' } }
        ]
      }
    ]
  }
}

Resolvers

GraphQL expects to find resolvers to stitch the response together. Each resolver is a function which joins one type to another, or which calculates a derived field.

feathers-plus generate graphql generates the resolver code needed for your GraphQL endpoint, and it generates up to 3 versions of the resolvers

The resolvers usually require a lot of code which must be meticulously correct. You will save a lot of time by having the generator write the resolvers.

For example, this is the resolver code generated for our simplist example for the straightforward case --- using normal Feathers service calls.

Comprehensive example

@feathers-plus/cli-generator-example contains a comprehensive GraphQL example produced with cli-plus. It's based on the following data schema

Schema for Example

vTitle


It has a test-harness running on the browser

Test Harness for Example

vTitle


You can select queries, modify them, run them and see the results.

The repo contains examples in both JavaScript and TypeScript, implementing the same test harness for:

  • Feathers service resolvers used with the NeDB database.
  • BatchLoader based resolvers used with the NeDB database.
  • Feathers service resolvers used with Sequelize backed by the SQLite database.
  • BatchLoader based resolvers used with Sequelize backed by the SQLite database.
  • Resolvers producing raw SQL statements used with Sequelize backed by the SQLite database.

You will note how many lines of code are required to implement GraphQL resolvers, and thankfully cli-plus generates them.

GraphQL extensions to models

Now that we understand what we expect to implement with the GraphQL endpoint, let's see how to accomplish it.

src/services/users/users.schema.?s contains an outline of GraphQL extensions to the users Feathers model.

// Define optional, non-JSON-schema extensions.
let extensions = {
  // GraphQL generation.
  graphql: {
    // !<DEFAULT> code: graphql_header
    // name: 'User',
    // service: {
    //   sort: { _id: 1 },
    // },
    // sql: {
    //   sqlTable: 'Users',
    //   uniqueKey: '_id',
    //   sqlColumn: {
    //     __authorId__: '__author_id__',
    //   },
    // },
    // !end
    discard: [
      // !code: graphql_discard // !end
    ],
    add: {
      // !<DEFAULT> code: graphql_add
      // __author__: { type: '__User__!', args: false, relation: { ourTable: '__authorId__', otherTable: '_id' } },
      // !end
    },
    // !code: graphql_more // !end
  },
};

Let's start filling it in.

Metadata

  • name The name of the GraphQL type for this service. It defaults to your answer in generate service for What would you call one row in the users database?
name: 'User',

Being included in the GraphQL endpoint

The service will not be included in the GraphQL endpoint until it's given a name.

Queries

Two queries are generated for each service. getUser is similar to Feathers users.get(id) and returns a single record. findUser is similar to users.find({ query: {...} }) and returns multiple records.

You can implement other queries by adding custom code to the resolver modules.

  • service This property must be defined for Feathers service-based resolvers to be generated.

    • service.sort How to sort the users records if users form the top level of the response.
service: {
  sort: { lastName: 1, firstName: 1 },
},

STOP

The service property must be defined for Feathers service-based resolvers to be generated for this service.

  • sql This property must be defined for resolvers producing raw SQL statements to be generated.

    • sql.sqlTable The name of the SQL table. Defaults to the service name.
    • sql.uniqueKey The name of the SQL column containing the primary key.
    • sql.sqlColumn Allows you to relate SQL column names to JavaScript variables if they differ, e.g. the SQL column name email_address to the JavaScript email.
sql : {
  email: 'email_address'
}

STOP

The sql property must be defined for resolvers producing raw SQL statements to be generated for this service.

join-monster

The generator uses join-monster to do the heavy lifting for SQL. You have to read and understand its documentation.

  • discard Fields not to be included in the GraphQL endpoint. If our users service had included a password field, it would be reasonable to have
discard: [
  // !code: graphql_discard
  'password' 
  // !end
],
  • add defines how other records are joined to this service, and how any computed fields are calculated.

Our Feathers model alone would produce the GraphQL type

type User {
  id: ID
  email: String!
  firstName: String!
  lastName: String!
  password: String
  roleId: ID!
}

Let's continue to expand the model to include joined records and a computed field.

Computed fields

You need only mention that a computed field exists.

    add: {
      // !code: graphql_add
      fullName: { type: 'String!', args: false },
      // !end
    },
  • fullName The name of the computed field.
  • type The GraphQL type of the result.
  • args: false The field is not permitted arguments in the GraphQL Query. The next section has more information on args.

GraphQL scalar types

GraphQL comes with a set of default scalar types out of the box:

  • Int A signed 32‐bit integer.
  • Float A signed double-precision floating-point value.
  • String A UTF‐8 character sequence.
  • Boolean true or false.
  • ID The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache. The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be human‐readable.

The GraphQL type system identifies the shape of a field

  • String The field is a String. It may also be null.
  • String! Its a String. null is not allowed.
  • [String] Its an array of Strings. An element may be null and, instead of an array, the field itself may be null.
  • [String!] Like [String] but the elements may not be null. null is still allowed instead of an array.
  • [String]! Like [String] but an array must exist.
  • [String!]! Like [String] but an array must exist and no element may be null.

Note we do not say how the field is calculated. cli-plus will generate resolver functions in src/services/service.resolvers.?s and /batchloader.resolvers.?s like

User: {
  // fullName: String!
  fullName:
    // !<DEFAULT>code: resolver-User-fullName-non
    (parent, args, content, ast) => { throw Error('GraphQL fieldName User.fullName is not calculated.'); },
    // !end
}    

And we customize it with the calculation.

User: {
  // fullName: String!
  fullName:
    // !code: resolver-User-fullName-non
    (parent, args, content, ast) => `${parent.firstName} ${parent.lastName}`,
    // !end
}    

We would customize the SQL resolvers in src/services/graphql/sql.metadata.?s

User: {
  sqlTable: 'Users',
  uniqueKey: 'uuid',
  fields: {
    // fullName: String!
    fullName: {
      // !code: fields-User-fullName-non
      sqlExpr: (tableName, args) => `${tableName}.firstName || ' ' || ${tableName}.lastName`
      // !end
    },
  },
}  

After defining the calculated field, our GraphQL type is

type User {
  id: ID
  email: String!
  firstName: String!
  lastName: String!
  password: String
  roleId: ID!
  fullName: String!
}

Joining records

Our schema is

Schema Get Started Example

vTitle


To each users record we can attach

  • The single roles record referred to in its users.roleId.
  • All the teams records containing its users.id in their teams.memberIds[].

To each roles record we can attach

  • All the users record containing its roles.id in users.roleId.

To each teams record we can attach

  • All the users records whose users.id is contained in its teams.memberIds.

Joining Roles record to Users

Our users model contains roleId, a foreign key to the roles model. We can join the proper roles record to the user record with

add: {
  // !code: graphql_add
  role: { type: 'Role', args: true, relation: { ourTable: 'roleId', otherTable: '_id' } },
  // !end
},
  • role The property added to users containing the roles record.
  • type Identifies the type of object the resolver function will return. Here it returns a roles object or null.

The GraphQL type system identifies the shape of a field

  • Role The field is a Role object. It may also be null.
  • Role! Its a Role object. null is not allowed.
  • [Role] Its an array of Role objects. An element may be null and, instead of an array, the field itself may be null.
  • [Role!] Like [Role] but the elements may not be null. null is still allowed instead of an array.
  • {Role]! Like [Role] but an array must exist.
  • [Role!]! Like [Role] but an array must exist and no element may be null.
  • args Determines if arguments are allowed for the field allowed in the GraphQL Query. cli-plus always generates Feathers compatible arguments:
role(query: JSON, params: JSON, key: JSON): Role
  • Where

    • key Equivalent to Feathers id, i.e. users.get(id).
    • query Equivalent to Feathers query, i.e. users.find({ query: {...} }).
    • params Equivalent to Feathers params, i.e. users.find(params). The query and params arguments are merged together as expected by Feathers.
  • relation The corresponding fields in each model which define the relationship.

    • ourTable The field name in the model whose metadata we are defining.
    • otherTable The field name in the other service.

Resolver functions

If ourTable and otherTable are both scalars, the resolver function would be similar to

roles.find({ query: { _id: parent.roleId }, paginate: false })

If ourTable had been an array, then the resolver function would be like

roles.find({ query: { _id: { $in: parent.roleIds } }, paginate: false })

If otherTable is an array, the resolver function will be similar to

if (!(content.cache.User && content.cache.User.teams)) {
 content.cache.User = content.cache.User || {};
 content.cache.User.teams =
   teams.find({ query: {...}, paginate: false }).then(extractAllItems);
}

return Promise.resolve(content.cache.User.teams)
 .then(res => res.filter(rec => rec.memberIds.indexOf(parent._id) !== -1));

STOP

ourTable and otherTable cannot both be arrays.

After defining roles, our GraphQL type would be

 type User {
   id: ID
   email: String!
   firstName: String!
   lastName: String!
   password: String
   roleId: ID!
   fullName: String!
   role(query: JSON, params: JSON, key: JSON): Role
 }

Joining Teams records to Users

We want to join all the teams records that a user belongs to.

add: {
  teams: { type: '[Team!]', args: true,
    relation: { ourTable: '_id', otherTable: 'memberIds' }, sort: { name: 1 }
  },
},

We are joining multiple records rather than a single one.

  • sort optionally, how to sort the records being joined.

After defining teams, our GraphQL type would be complete.

type User {
  id: ID
  email: String!
  firstName: String!
  lastName: String!
  password: String
  roleId: ID!
  fullName: String!
  role(query: JSON, params: JSON, key: JSON): Role
  teams(query: JSON, params: JSON, key: JSON): [Team!]
}

The final result GraphQL extension for users is:

// Define optional, non-JSON-schema extensions.
let extensions = {
  // GraphQL generation.
  graphql: {
    // !code: graphql_header
    name: 'User',
    service: {
      sort: { lastName: 1, firstName: 1 },
    },
    // !end
    discard: [
      // !code: graphql_discard // !end
    ],
    add: {
      // !code: graphql_add
      fullName: { type: 'String!', args: false },
      role: { type: 'Role', args: true,
        relation: { ourTable: 'roleId', otherTable: '_id' }
      },
      teams: { type: '[Team!]', args: true,
        relation: { ourTable: '_id', otherTable: 'memberIds' }, sort: { name: 1 }
      },
      // !end
    },
    // !code: graphql_more // !end
  },
};

Joins for the roles and teams service.

The GraphQL extension for the roles service is similarly:

// Define optional, non-JSON-schema extensions.
let extensions = {
  // GraphQL generation.
  graphql: {
    // !code: graphql_header
    name: 'Role',
    service: {
      sort: { name: 1 },
    },
    // !end
    discard: [
      // !code: graphql_discard // !end
    ],
    add: {
      // !code: graphql_add
      users: { type: '[User!]', args: false,
        relation: { ourTable: '_id', otherTable: 'roleId' }
      },
      // !end
    },
    // !code: graphql_more // !end
  },
};

And for teams is

// Define optional, non-JSON-schema extensions.
let extensions = {
  // GraphQL generation.
  graphql: {
    // !code: graphql_header
    name: 'Team',
    service: {
      sort: { name: 1 },
    },
    // !end
    discard: [
      // !code: graphql_discard // !end
    ],
    add: {
      // !code: graphql_add
      members: { type: '[User!]', args: false,
        relation: { ourTable: 'memberIds', otherTable: '_id' },
        sort: { lastName: 1, firstName: 1 }
      },
      // !end
    },
    // !code: graphql_more // !end
  },
};

generate graphql

We have the GraphQL extensions coded and, during generate service, we indicated which services are to be included in the GraphQL endpoint. We can now generate that endpoint.

feathers-plus generate graphql
Prompts 'generate graphql'

vTitle


  • How should Queries be completed? Different versions of GraphQL resolver functions may be generated.
    • Using standalone Feathers service calls is the default and the simplest approach. It uses plain Feathers service calls.
    • Using BatchLoaders uses caching to eliminate multiple reads of the same information. It also batches individual services calls together to reduce the number of database requests made. BatchLoaders typically make an order of magnitude fewer requests, i.e. 3 instead of 30, than the previous option.
    • Using dynamic SQL statements is available only for SQL databases. These resolvers generate a raw SQL statement, so the query is fullfilled by one request to the database.

SQL statments

The raw SQL statement may be very large for a complex query, and that may result in performance problems for such queries.

You should normally use this option when you have good SQL experience. You will also have to become familiar with join-monster which is used by the generated code.

Practicality

We recommend you get your app working using the standalone services option because it's the easiest to debug. You can switch to BatchLoaders if your server is under load or if you need better performance.

It is not clear how more performant raw SQL statements are over BatchLoaders. You can evaluate them if you want better performance than that provided by BatchLoaders.

  • Which path should the service be registered on? Default is /graphql.

  • Does the service require authentication? Default is no. As the query is being resolved, authentication is performed for any service being accessed, so these services are not exposed if the GraphQL endpoint itself is not secured.

Folders

The generator adds some modules to the JS folder or TS one.

Folders after 'generate graphql' with JavaScript

vTitle


Folders after 'generate graphql' with TypeScript

vTitle


  • graphql.schemas.?s The GraphQL type definitions.
  • graphql.service.?s Configures the GraphQL service for the strategy chosen in the How should Queries be completed? question.
  • graphql.hooks.?s Feathers hooks for the GraphQL service.
  • graphql.interfaces.ts - TypeScript interfaces for GraphQL resolvers.
  • service.resolvers.?s Resolver functions for the Using standalone Feathers service calls strategy.
  • batchloader.resolvers.?s Resolver functions for the Using BatchLoaders strategy.

Several modules are used for the Using dynamic SQL statements strategy:

  • sql.resolvers.?s Resolver functions for the strategy.
  • sql.metadata.?s Meta data used by join-monster to dynamically construct the raw SQL statement.
  • sql.execute.sequelize.?s Execute a raw SQL statement using Sequelize.
  • sql.execute.knex.?s Execute a raw SQL statement with Knex.
  • sql.execute.custom.?s Execute a raw SQL statement in a custom environment.

All those modules

Only one of service.resolvers.?s, batchloader.resolvers.?s or sql.**.?s is/are being used at any time. The code generated in graphql.service.?s calls the right modules for the How should Queries be completed? selected.

The modules are all generated so you may include all the custom code you need for all of them. This allows you to rerun generate graphql at any time to change strategies.

generate hook

We can generate hooks designed either to work with one particular service, or to be used with multiple services.

Generate a hook for one service

Let's create a hook intended to verify the email fields in the users service.

feathers-plus generate hook
Prompts 'generate hook' for one service

vTitle


  • What is the name of the hook? A valid file name or a camel-case name.
  • The hook will be used with: Choose One service.
  • Which service will this hook be used with? Select from the list of generated services.

Folders

The generator adds some modules to the JS folder or TS one.

Folders after 'generate hook' for users service, with JavaScript

vTitle


Folders after 'generate hook' for users service, with TypeScript

vTitle


  • src/services/users/hooks/verify-email.?s Hooks for a single service are located within the folder for that service.
  • test/services/users/hooks/verify-email.unit.test.?s A unit test module is also generated for the hook. You can read more about hook unit tests in the generate test section.

The hook is automatically imported in the users.hooks.?s module.






 


// src/services/users/users.hooks.js
// Hooks for service `users`. (Can be re-generated.)
const commonHooks = require('feathers-hooks-common');
const { authenticate } = require('@feathersjs/authentication').hooks;
// eslint-disable-next-line no-unused-vars
const verifyEmail = require('./hooks/verify-email');
// !code: imports // !end

Positioning hooks

The generator cannot know in which order you want the hook to execute. It would be uncommon for the right location be at the end of, say, the before hooks. So its your responsibly to use the imported hook where it belongs.

Generate a hook for all/multiple services

Let's create a hook intended to convert strings like 'null' found in REST requests to the JavaScript scalar null required by service calls.

Prompts 'generate hook' for all/multiple services

vTitle


  • What is the name of the hook? A valid file name or a camel-case name.
  • The hook will be used with: Choose Multiple services.
  • Which services will this hook be used with?
    • I will add the hook where it is needed: The hook module is created but you are expected to import it where its needed.
    • Use with all services as an app hook: The hook module is imported into src/app.hooks.?s which contains hooks run for all services.
    • service names: Select one or more services which will use the hook. The hook will be imported into their src/services/serviceName/serviceName.hook.?s modules.

Folders

The generator adds some modules to the JS folder or TS one.

Folders after 'generate hook' for app, with JavaScript

vTitle


Folders after 'generate hook' for app, with TypeScript

vTitle


  • src/hooks/normalize-query.?s All hooks used by multiple services are located in this folder.
  • test/hooks/normalize-query.unit.test.?s A unit test is automatically created for the hook. The tests for hooks used by multiple services are all located in this folder.

Since we created a Use with all services as an app hook hook, its automatically imported into the src/app.hooks.?s module.






 
// src/app.hooks.js
// Application hooks that run for every service. (Can be re-generated.)
const commonHooks = require('feathers-hooks-common');
// eslint-disable-next-line no-unused-vars
const normalizeQuery = require('./hooks/normalize-query');

generate test

Test can be generated for

  • Hooks. Both unit tests and integration ones.
  • Services. Both tests running on the server (light integration), and ones running client/server (end-to-end integration).
  • Authentication. Two tests can be generated. One tests that basic authentication works using the user-entity. The other tests that every method on every service uses the authentication you expect.

All these test are generated using

feathers-plus generate test
Initial prompts 'generate test'

vTitle


  • hook - unit Generate a unit test for a hook.
  • hook - integration Generate an integration test for a hook
  • service - server Generate a test running solely on the server.
  • service - client Generate a test where requests are started on a client, and fullfilled on the server.
  • authentication - base Generate a test that checks authentication exists.
  • authentication - services Generate a test which checks authentication for every method on every service is what you expect.

Unit test for a hook

Prompts 'generate test' for a hook unit test

vTitle


  • Which hook is being tested? Select the hook you want tested.

:::warn Restriction Presently test can be generated only for generated hooks.

If you want to generate tests for manually created hooks, generate a test for a generated hook and then rename the generated test module. :::

Integration test for a hook

Prompts 'generate test' for a hook integration test

vTitle


Test a service using the server

Prompts 'generate test' for a service server test

vTitle


  • What is the name of the service being tested? Choose the service to be tested.

Test a service using client/server

Prompts 'generate test' for a service client/server test

vTitle


All client/server tests are configurable to your environment. config/default.json contains the options.

Client/server

All generated client/server tests use these options. Fell free to use them yourself.

Test that checks authentication exists.

More prompts 'generate test' for base authentication

vTitle


Test which check authentication for every service

More prompts 'generate test' for authentication for services'

vTitle


Folders

The generator adds some modules to the JS folder or TS one.

Folders after 'generate hook' for users service, with JavaScript

vTitle


Folders after 'generate hook' for users service, with TypeScript

vTitle


  • test/hooks The tests for all/multiple hooks are located in this folder.
  • test/services/serviceName/hooks The tests for individual hooks are located in this folder.
  • test/services/serviceName The tests for services are located in this folder.
  • test The authentication tests are located in this folder.

More information

More information regarding generate test is available in the article Automatic tests with feathers-plus/cli.

What We Have Accomplished

REST and GraphQL are totally different. Each is dope if used for the right thing.

cli-plus allows us to specify our services (a REST concept) along with their models and relations. It then creates our GraphQL endpoint, and you'll infrequently have to customize that.

In Short

FeathersJS, using cli-plus, now supports both REST and GraphQL architectural concepts and their query languages.

generate all

cli-plus will change over time: bug fixes, enhancements, retooling for new version of FeathersJS. You can upgrade your app to the latest version of cli-plus by running

feathers-plus generate all
Prompts 'generate all'

vTitle


This will regenerate most of the modules, keeping you custom code, while generating the latest cli-plus boilerplate.

Be Safe

You may want to backup you project before running generate all until you feel comfortable no longer doing so.

Modules Regeneration

cli-plus will not override certain modules if they already exist. We have already seen the names of these when we looked at generate options

Prompts 'generate options'

vTitle


Freezing modules

You can prevent cli-plus from changing other modules it would otherwise regenerate. feathers-gen-specs.json contains all the information needed to recreate your app (in addition to your custom code of course). It starts with:

{
  "options": {
    "ver": "1.0.0",
    "inspectConflicts": false,
    "semicolons": true,
    "freeze": [],
    "ts": false
  },

cli-plus will not overwrite any module names you add to freeze. For example

"freeze": ["src/hooks/logger.js"]

Custom code

You can list all the custom code in your app using

feathers-plus generate codelist
Prompts 'generate codelist'

vTitle


Next Steps

You should try generating a simple app now. Get it running first without a GraphQL endpoint, then add the endpoint in.

You can find more specialized information in

  • Guide - More details on particular subjects.
  • API - The API for various functions called by the generated code. You may want to use them yourself in your your custom code.
  • JSON-schema - An introduction to the extensive JSON-schema ecosystem.
  • Maintainers - Useful information if you want to contribute to this project. Design ideas for future enhancements.

Good luck!