create-graphql-server-query-arguments
Build query arguments for filter and orderBy MongoDB queries.
Added 60 test cases, which pass against my local create-graphql-server instance. These tests are referenced in create-graphql-server test runs later.
TODO:
- There can be more test cases, with complex queries and a critical review of the tests. Add them to (src/index-test-cases.js).
- Implement a test app within the package, to run these test cases against. Used so far my local server to test with.
Purpose
You build a GraphQL server with the npm package "create-graphql-server", which serves as a backend to web applications. This "create-graphql-server" generates schemas, resolvers and models for an express-js server.
As soon as you are building the web application on top of this server, you want to access this backend server with specific GraphQL queries. Sometimes you want to set filters, to get just filtered records. Sometimes you want to sort data by different fields in ascending or descending order. Sometimes you want just pages of data with the first ten data records, or just the second page after the first ten records and so on.
In order to enable such accesses to your GraphQL server backend, the schema needs to provide query arguments such as:
- filter
- orderBy
- limit
- skip
TODO: as enhanced version of limit and skip:
- first
- before
- last
- after
Additionally, your data model must know, how to map these query arguments into valid database queries for the mongoDB database.
That's the purpose of this module.
- it provides a function for the schema generator, to generate additional query arguments
- it provides a function for basic types for all arguments later
- it provides a function for the data model, to map query arguments, into a database query
GraphQL query argument to mongoDB mapper:
const baseQuery sortQuery skip limit =
GraphQL schema Query argument generator:
;
GraphQL schema Query argument generator:
const enhancedOutputSchema = ;
It provides the following query arguments:
orderBy
All fields of the type definitions are automatically added to the orderBy sort field selection, except for associations to other types.
limit
A limit argument is added, to choose the number of documents/records the query should return.
skip
A skip argument is added, to skip a number of found records, not to be returned by the query.
filter
The following filter query arguments are added to list types, which you can use to build complex queries:
- eq
- all
- ne
- in
- nin
- lt
- lte
- gt
- gte
- regex
- contains
- starts_with
- ends_with
- not_contains
- not_starts_with
- not_ends_with
- not_in
- exists
- not
- type
- AND
- NOR
- OR
Installation
Installation Part 1 -- add the module to create-graphql-server project
The create-graphql-server generator actually consists out of three parts:
- root directory, the generator with directory "generate" and its according "package.json"
- skel directory, contains the skeleton of the future "to-be-generated-app", also with its "package.json"
- test/output-app directory, contains a generated test application, which is like the app in skel, but with already generated parts. This is used for test runs, to check, if it produces valid code.
In order to get the whole up and running, we have to consider all three parts.
In the "root" directory:
yarn add create-graphql-server-query-arguments
In the "test/output-app" directory
yarn add create-graphql-server-query-arguments
In the "skel/package.json", we have to update only the package.json, that it looks the same like in "test/output-app". Just copy the package.json like so and replace
cp <path>/test/output-app/package.json <path>/skel/package.json
Installation Part 2 -- add it to the server for the mongoDB accesses
Add this module to your express server in "create-graphql-server/skel/server/index.js" and also in the "test/output-app/server/index.js" and provide it to your data model by:
...;...
... further below in both files, also add it it your your data model context...
...app...
Now you can access it in your data models with "this.context.prepareQueries", e.g. in your "model/User.js":
{ const baseQuery sortQuery skip limit = thiscontext; // <=== const authQuery = ; const finalQuery = ...baseQuery ...authQuery ; return thiscollection ;}
Be sure, that also the resolver(s) pass on all "args" to your model method "find".
If you forget to sync your changes in both the "skel" directory and your "test/output-app" directory, your test runs will fail. It compares the generated app files from "skel" with those in the "output-app" files.
Installation Part 3 -- General Type Definitions for all arguments, but can be defined only once
Add to files "skel/schema/index.js" and "test/output-app/schema/index.js":
...; // <=== add this line...const typeDefs = ` scalar ObjID type Query { # A placeholder, please ignore __placeholder: Int } type Mutation { # A placeholder, please ignore __placeholder: Int } type Subscription { # A placeholder, please ignore __placeholder: Int }`; typeDefs; // <=== add this line ;...
Caution: Do the same again in the "test/output-app/schema/index.js" to have proper test runs.
Installation Part 4 -- Generator for Schema for individual argument type definitions
Add to file "generate/schema/index.js" the following statements:
If you don't have installed "create-graphql-server-authorization", use this:
...; // <== here...... // if you have NOT installed create-graphql-server-authorization add this: const outputSchemaWithArguments = ; // <== here return outputSchemaWithArguments; // <== here}
If you have installed also "create-graphql-server-authorization", use this instead:
...... // if you have also create-graphql-server-authorization installed use this: const outputSchemaWithAuth = ; const outputSchemaWithArguments = ; // <== here return outputSchemaWithArguments; // <== here}
Add those types to your outputSchema.
Installation Part 5 --- adjust model and resolver templates
In "generate/model/templates" and "generate/resolvers/templates" adjust the templates: "default_default.template" and "default_user.template". (If you use create-graphql-server-authorization as well, you have to create additionally the following two files: "authorize_default.template" and "authorize_user.template").
Adjust "generate/model/templates/default/default_default.template"
from...
... { const finalQuery = ...baseQuery createdAt: $gt: lastCreatedAt ; return thiscollection ; }...
...to...
... { const baseQuery sortQuery skip limit = thiscontext; const finalQuery = ...baseQuery ; thiscontextlog; return thiscollection ; }...
Adjust "generate/model/templates/user/default_user.template"
from....
... { const finalQuery = ...baseQuery createdAt: $gt: lastCreatedAt ; return thiscollection ; }...
...to...
... { const baseQuery sortQuery skip limit = thiscontext; const finalQuery = ...baseQuery ; thiscontextlog; return thiscollection ; }...
Change "generate/resolvers/templates/default/default_default.template"
from...
...Query: typeNames(root, { lastCreatedAt, limit }, { {{TypeName}}, me }) { return {TypeName}; ...
...to...
...Query: typeNames(root, args, { {{TypeName}}, me }) { return {TypeName}; ...
With Authorization create-graphql-server-authorization
OPTIONAL: If you are using create-graphql-server-authorization as well, you have to create additional files:
Add "generate/model/templates/default/authorize_default.template":
/* eslint-disable prettier */; class TypeName { thiscontext = context; thiscollection = contextdb; thispubsub = contextpubsub; const me User = context; ; } async { const log = ; if !thisauthorizedLoader log; return null; return await thisauthorizedLoader; } { const baseQuery sortQuery skip limit = thiscontext; const authQuery = ; const finalQuery = ...baseQuery ...authQuery ; thiscontextlog; return thiscollection ; }#each singularFields> defaultSingularField /each#each paginatedFields> defaultPaginatedField /each { return thiscontextUser; } { return thiscontextUser; } async { const docToInsert = Object; ; const id = await thiscollectioninsertedId; if !id throw `insert {{typeName}} not possible.`; thiscontextlog; const insertedDoc = this; thispubsub; return insertedDoc; } async { const docToUpdate = $set: Object ; const baseQuery = _id: id ; const authQuery = ; const finalQuery = ...baseQuery ...authQuery ; const result = await thiscollection; if resultresultok !== 1 || resultresultn !== 1 throw `update {{typeName}} not possible for .`; thiscontextlog; thisauthorizedLoaderclearid; const updatedDoc = this; thispubsub; return updatedDoc; } async { const baseQuery = _id: id ; const authQuery = ; const finalQuery = ...baseQuery ...authQuery ; const result = await thiscollection; if resultresultok !== 1 || resultresultn !== 1 throw `remove {{typeName}} not possible for .`; thiscontextlog; thisauthorizedLoaderclearid; thispubsub; return result; }
Adjust "generate/model/templates/user/authorize_user.template":
/* eslint-disable prettier */;;const SALT_ROUNDS = 10; class TypeName { thiscontext = context; thiscollection = contextdb; thispubsub = contextpubsub; thisauthRole = UserauthRole; const me = context; ; } static { return typeName && typeNameroleField ? typeNameroleField : null; } async { const log = ; if !thisauthorizedLoader log; return null; return await thisauthorizedLoader; } { const baseQuery sortQuery skip limit = thiscontext; const authQuery = ; const finalQuery = ...baseQuery ...authQuery ; thiscontextlog; return thiscollection ; }#each singularFields> defaultSingularField /each#each paginatedFields> defaultPaginatedField /each { return thiscontextUser; } { return thiscontextUser; } async { // We don't want to store passwords in plaintext const password ...rest = doc; const hash = await bcrypthashpassword SALT_ROUNDS; let docToInsert = Object; ; docToInsert = ; const id = await thiscollectioninsertedId; if !id throw `insert {{typeName}} not possible.`; thiscontextlog; const insertedDoc = this; thispubsub; return insertedDoc; } async { const docToUpdate = $set: Object ; const baseQuery = _id: id ; const authQuery = ; docToUpdate$set = ; const finalQuery = ...baseQuery ...authQuery ; const result = await thiscollection; if resultresultok !== 1 || resultresultn !== 1 throw `update {{typeName}} not possible for .`; thiscontextlog; thisauthorizedLoaderclearid; const updatedDoc = this; thispubsub; return updatedDoc; } async { const baseQuery = _id: id ; const authQuery = ; const finalQuery = ...baseQuery ...authQuery ; const result = await thiscollection; if resultresultok !== 1 || resultresultn !== 1 throw `remove {{typeName}} not possible for .`; thiscontextlog; thisauthorizedLoaderclearid; thispubsub; return result; }
Add "generate/resolvers/templates/default/authorize_default.template":
/* eslint-disable prettier *//* eslint comma-dangle: [2, "only-multiline"] */const resolvers = TypeName: { return typeName_id; }#each singularFields> defaultSingularField /each#each paginatedFields> defaultPaginatedField /each { return TypeName; } { return TypeName; } Query: typeNames(root, args, { {{TypeName}}, me }) { return {TypeName}; typeNameroot id TypeName me return TypeName; Mutation: async createTypeNameroot input TypeName me return await TypeName; async updateTypeNameroot id input TypeName me return await TypeName; async removeTypeNameroot id TypeName me return await TypeName; Subscription: typeNameCreated: typeName typeName typeNameUpdated: typeName typeName typeNameRemoved: id => id }; ;
Installation Part 6 --- For test runs
The "test/output-app" expects now also different schema files, as we added these additional query arguments. So they have to be also in the test app, otherwise our tests will fail. You don't have to do this manually. In create-graphql-server "bin/gentest.js" we have command line command, which is generating all the required files as test files in a temp directory, and if you are using sublime it will show them in sublime. Run it with...
yarn gentest # defaults to test/input/User.graphql AND: yarn gentest test/input/Tweet.graphql
With that you get also all schema files generated as they should look like with the new extended "enhanceSchemaWithQueryArguments" logic.
Copy the generated .graphql files and overwrite the files in "test/output-app/schema/.graphql". Copy the generated .model.js files and overwrite the files in "test/output-app/model/.js" Copy the generated *.resolver.js files and overwrite the files in "test/output-app/resolvers"
Finally
If you have succeeded an all the following test runs are ok, you did well. Congratulations!
yarn end-to-end-testyarn output-app-generation-testyarn test-add-update-remove
If you are having troubles somewhere, have a look into the running example at: tobkle/create-graphql-server branch: Authorization+Arguments
Documentation
Tests
yarn test
Or in create-graphql-server package itself:
yarn end-to-end-arguments-test
Contributing
In lieu of a formal style guide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code.
Example Queries
Have a look in the test directory to see more: index-test-cases