diff --git a/.circleci/config.yml b/.circleci/config.yml index 71851945da..ca6cb58ba6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,6 +12,11 @@ jobs: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgresql POSTGRES_DB: users + - image: circleci/mongo:latest + environment: + MONGO_INITDB_DATABASE: users + MONGO_INITDB_ROOT_USERNAME: mongodb + MONGO_INITDB_ROOT_PASSWORD: mongo steps: - checkout - restore_cache: diff --git a/.gitignore b/.gitignore index d04edc6cfd..1d4e357deb 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ lib yarn.lock integration/test.sqlite integration/output/ +integration/output-postgres/ +integration/output-mongo/ diff --git a/integration/.graphqlrc.yml b/integration/.graphqlrc.yml deleted file mode 100644 index e1188024ca..0000000000 --- a/integration/.graphqlrc.yml +++ /dev/null @@ -1,24 +0,0 @@ -schema: ./output/schema/*.graphql -documents: ./output/client/**/*.graphql -extensions: - # Graphback configuration - graphback: - ## Input schema` - model: ./mock.graphql - ## Global configuration for CRUD generator - crud: - create: true - update: true - findOne: true - find: true - delete: true - subCreate: true - subUpdate: true - subDelete: true - ## Codegen plugins - plugins: - graphback-schema: - outputPath: ./output/schema/schema.graphql - graphback-client: - format: 'graphql' - outputFile: './output/client/graphback.graphql' diff --git a/integration/package.json b/integration/package.json index cb5eb193a3..c24192b6ba 100644 --- a/integration/package.json +++ b/integration/package.json @@ -17,7 +17,6 @@ }, "devDependencies": { "rimraf": "3.0.2", - "sqlite3": "4.2.0", "ts-node": "8.10.2", "tsutils": "3.17.1", "typescript": "3.9.5", @@ -27,6 +26,7 @@ "apollo-server-testing": "2.15.0", "jest": "26.1.0", "knex": "0.21.1", + "mongodb": "3.5.9", "node-fetch": "2.6.0" }, "license": "Apache-2.0" diff --git a/integration/tests/__snapshots__/runtime-workflow.ts.snap b/integration/tests/__snapshots__/runtime-workflow-postgres.ts.snap similarity index 100% rename from integration/tests/__snapshots__/runtime-workflow.ts.snap rename to integration/tests/__snapshots__/runtime-workflow-postgres.ts.snap index 657644f419..586536feca 100644 --- a/integration/tests/__snapshots__/runtime-workflow.ts.snap +++ b/integration/tests/__snapshots__/runtime-workflow-postgres.ts.snap @@ -137,7 +137,7 @@ Object { "nullable": true, "type": "string", }, - "metadata" => Object { + "note" => Object { "annotations": Object {}, "args": Array [], "autoIncrementable": false, @@ -146,15 +146,15 @@ Object { "foreign": Object { "columnName": "id", "field": "id", - "tableName": "commentmetadata", - "type": "CommentMetadata", + "tableName": "note", + "type": "Note", }, "isPrimaryKey": false, - "name": "metadataId", + "name": "noteId", "nullable": true, "type": "integer", }, - "note" => Object { + "metadata" => Object { "annotations": Object {}, "args": Array [], "autoIncrementable": false, @@ -163,11 +163,11 @@ Object { "foreign": Object { "columnName": "id", "field": "id", - "tableName": "note", - "type": "Note", + "tableName": "commentmetadata", + "type": "CommentMetadata", }, "isPrimaryKey": false, - "name": "noteId", + "name": "metadataId", "nullable": true, "type": "integer", }, @@ -222,11 +222,11 @@ Object { "foreign": Object { "columnName": "id", "field": "id", - "tableName": "commentmetadata", - "type": "CommentMetadata", + "tableName": "note", + "type": "Note", }, "isPrimaryKey": false, - "name": "metadataId", + "name": "noteId", "nullable": true, "type": "integer", }, @@ -239,11 +239,11 @@ Object { "foreign": Object { "columnName": "id", "field": "id", - "tableName": "note", - "type": "Note", + "tableName": "commentmetadata", + "type": "CommentMetadata", }, "isPrimaryKey": false, - "name": "noteId", + "name": "metadataId", "nullable": true, "type": "integer", }, @@ -448,7 +448,7 @@ Object { "nullable": true, "type": "string", }, - "metadata" => Object { + "note" => Object { "annotations": Object {}, "args": Array [], "autoIncrementable": false, @@ -457,15 +457,15 @@ Object { "foreign": Object { "columnName": "id", "field": "id", - "tableName": "commentmetadata", - "type": "CommentMetadata", + "tableName": "note", + "type": "Note", }, "isPrimaryKey": false, - "name": "metadataId", + "name": "noteId", "nullable": true, "type": "integer", }, - "note" => Object { + "metadata" => Object { "annotations": Object {}, "args": Array [], "autoIncrementable": false, @@ -474,11 +474,11 @@ Object { "foreign": Object { "columnName": "id", "field": "id", - "tableName": "note", - "type": "Note", + "tableName": "commentmetadata", + "type": "CommentMetadata", }, "isPrimaryKey": false, - "name": "noteId", + "name": "metadataId", "nullable": true, "type": "integer", }, @@ -533,11 +533,11 @@ Object { "foreign": Object { "columnName": "id", "field": "id", - "tableName": "commentmetadata", - "type": "CommentMetadata", + "tableName": "note", + "type": "Note", }, "isPrimaryKey": false, - "name": "metadataId", + "name": "noteId", "nullable": true, "type": "integer", }, @@ -550,11 +550,11 @@ Object { "foreign": Object { "columnName": "id", "field": "id", - "tableName": "note", - "type": "Note", + "tableName": "commentmetadata", + "type": "CommentMetadata", }, "isPrimaryKey": false, - "name": "noteId", + "name": "metadataId", "nullable": true, "type": "integer", }, diff --git a/integration/tests/runtime-workflow-mongo.ts b/integration/tests/runtime-workflow-mongo.ts new file mode 100644 index 0000000000..4b54f89d8a --- /dev/null +++ b/integration/tests/runtime-workflow-mongo.ts @@ -0,0 +1,434 @@ +/* eslint-disable no-null/no-null */ +/* eslint-disable no-shadow */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable import/no-extraneous-dependencies */ +import { mkdirSync, readFileSync, rmdirSync } from 'fs'; +import * as path from 'path'; +import { ApolloServer } from "apollo-server"; +import { createTestClient, ApolloServerTestClient } from 'apollo-server-testing'; +import { loadConfig } from 'graphql-config'; +import { loadDocuments } from '@graphql-toolkit/core'; +import { GraphQLFileLoader } from '@graphql-toolkit/graphql-file-loader'; +import { buildGraphbackAPI, GraphbackAPI } from "graphback"; +import { DocumentNode } from 'graphql'; +import { createMongoDbProvider } from "../../packages/graphback-runtime-mongodb" +import { MongoClient, Db } from 'mongodb'; +import { SchemaCRUDPlugin } from '../../packages/graphback-codegen-schema'; +import { ClientCRUDPlugin } from '../../packages/graphback-codegen-client'; + +/** global config */ +let db: Db; +let mongoClient: MongoClient; +let server: ApolloServer; +let client: ApolloServerTestClient; +let graphbackApi: GraphbackAPI; + +let documents: DocumentNode; + +let notesId = []; +let commentId = []; +let metadataId = []; + +const modelText = readFileSync("./mock.graphql").toString(); + +beforeAll(async () => { + try { + mkdirSync("./output-mongo"); + mkdirSync("./output-mongo/client") + + mongoClient = new MongoClient('mongodb://mongodb:mongo@localhost:27017/users?authSource=admin', { useUnifiedTopology: true }); + await mongoClient.connect(); + db = mongoClient.db('users'); + graphbackApi = buildGraphbackAPI(modelText, { + dataProviderCreator: createMongoDbProvider(db), + plugins: [ + new SchemaCRUDPlugin({outputPath: "./output-mongo/schema/schema.graphql"}), + new ClientCRUDPlugin({format: 'graphql', outputFile: './output-mongo/client/graphback.graphql'}) + ] + }); + + await seedDatabase(db); + + const source = await loadDocuments(path.resolve(`./output-mongo/client/**/*.graphql`), { + loaders: [ + new GraphQLFileLoader() + ] + }); + documents = source[0].document; + } catch (e) { + console.log(e); + throw e; + } +}) + +beforeEach(() => { + const { typeDefs, resolvers, contextCreator } = graphbackApi; + server = new ApolloServer({ + typeDefs, + resolvers, + context: contextCreator + }); + + client = createTestClient(server); +}) + +afterEach(() => server.stop()) + +afterAll(async () => { + rmdirSync(path.resolve('./output-mongo'), { recursive: true }); + const dropCollections = ["note", "comment", "commentmetadata"].map((name: string) => db.dropCollection(name)); + await Promise.all(dropCollections); + return mongoClient.close(); +}); + +async function seedDatabase(db: Db) { + const notes = [ + { + title: 'Note A', + description: 'Note A Description' + }, + { + title: 'Note B', + description: 'Note B Description' + } + ] + + for (const note of notes) { + const { ops } = await db.collection("note").insertOne(note); + notesId.push(ops[0]._id.toString()); + } + + const commentMetadata = [ + { + opened: true + }, + { + opened: false + } + ] + + for (const metadata of commentMetadata) { + const {ops} = await db.collection('commentmetadata').insertOne(metadata); + metadataId.push(ops[0]._id.toString()); + } + + const comments = [ + { + text: 'Note A Comment', + description: 'Note A Comment Description', + noteId: notesId[0], + metadataId: metadataId[0] + }, + { + text: 'Note A Comment 2', + description: 'Note A Comment Description', + noteId: notesId[0], + metadataId: metadataId[1] + } + ] + + for (const comment of comments) { + const {ops} = await db.collection('comment').insertOne(comment); + commentId.push(ops[0]._id.toString()) + } +} + +const getConfig = async () => { + const config = await loadConfig({ + rootDir: process.cwd(), + extensions: [ + () => ({ name: 'graphback' }) + ] + }); + + const projectConfig = config.getDefault(); + const graphbackConfig = projectConfig.extension('graphback'); + + return { projectConfig, graphbackConfig }; +} + +test('Find all notes', async () => { + const { data } = await client.query({ operationName: 'findNotes', query: documents }); + + expect(data).toBeDefined(); + expect(data.findNotes).toEqual({ + items: [ + { + id: notesId[0], + title: 'Note A', + description: 'Note A Description', + comments: [ + { + id: commentId[0], + text: 'Note A Comment', + description: 'Note A Comment Description' + }, + { + id: commentId[1], + text: 'Note A Comment 2', + description: 'Note A Comment Description' + } + ] + }, + { + id: notesId[1], + title: 'Note B', + description: 'Note B Description', + comments: [] + } + ], + limit: null, + offset: 0, + count: 2 + }) +}) + +test('Find all notes except the first', async () => { + const { data } = await client.query({ + operationName: 'findNotes', + query: documents, + variables: { page: { offset: 1 } } + }); + + expect(data).toBeDefined(); + expect(data.findNotes).toEqual({ + items: [ + { + id: notesId[1], + title: 'Note B', + description: 'Note B Description', + comments: [] + } + ], + limit: null, + offset: 1, + count: 2 + }) +}) + +test('Find at most one note', async () => { + const { data } = await client.query({ + operationName: 'findNotes', + query: documents, + variables: { page: { limit: 1 } } + }); + + expect(data).toBeDefined(); + expect(data.findNotes).toEqual({ + items: [ + { + id: notesId[0], + title: 'Note A', + description: 'Note A Description', + comments: [ + { + id: commentId[0], + text: 'Note A Comment', + description: 'Note A Comment Description' + }, + { + id: commentId[1], + text: 'Note A Comment 2', + description: 'Note A Comment Description' + } + ] + }, + ], + limit: 1, + offset: 0, + count: 2 + }) +}) + +test('Find all comments', async () => { + const { data } = await client.query({ operationName: "findComments", query: documents }); + + expect(data).toBeDefined(); + expect(data.findComments).toEqual({ + items: [ + { + id: commentId[0], + text: 'Note A Comment', + description: 'Note A Comment Description', + note: { + id: notesId[0], + title: 'Note A', + description: 'Note A Description' + }, + metadata: { + id: metadataId[0], + opened: true + } + }, + { + id: commentId[1], + text: 'Note A Comment 2', + description: 'Note A Comment Description', + note: { + id: notesId[0], + title: 'Note A', + description: 'Note A Description' + }, + metadata: { + id: metadataId[1], + opened: false + } + } + ], + limit: null, + offset: 0, + count: 2 + }) +}) + +test('Note 1 should be defined', async () => { + const response = await getNote(notesId[0], client); + expect(response.data).toBeDefined(); + const notes = response.data.getNote; + expect(notes).toEqual({ + id: notesId[0], + title: 'Note A', + description: 'Note A Description', + comments: [ + { + id: commentId[0], + text: 'Note A Comment', + description: 'Note A Comment Description' + }, + { + id: commentId[1], + text: 'Note A Comment 2', + description: 'Note A Comment Description' + } + ] + }); +}) + +test('Find at most one comment on Note 1', async () => { + const response = await client.query({ + operationName: "findComments", + query: documents, + variables: { filter: { noteId: { eq: notesId[0] } }, page: { limit: 1 } } + }); + + expect(response.data).toBeDefined() + const comments = response.data.findComments + expect(comments.items).toHaveLength(1); + expect(comments.items).toEqual([ + { + id: commentId[0], + text: 'Note A Comment', + description: 'Note A Comment Description', + metadata: { + id: metadataId[0], + opened: true, + }, + note: { + description: "Note A Description", + id: notesId[0], + title: "Note A", + }, + } + ]) +}) + +test('Find comments on Note 1 except first', async () => { + const response = await client.query({ + operationName: "findComments", + query: documents, + variables: { filter: { noteId: { eq: notesId[0] } }, page: { offset: 1 } } + }); + + expect(response.data).toBeDefined() + const notes = response.data.findComments + expect(notes.items).toHaveLength(1); + expect(notes).toEqual({ + items: [ + { + id: commentId[1], + text: 'Note A Comment 2', + description: 'Note A Comment Description', + metadata: { + "id": metadataId[1], + "opened": false, + }, + note: { + "description": "Note A Description", + "id": notesId[0], + "title": "Note A", + }, + } + ], + limit: null, + offset: 1, + count: 2 + }) +}) + +test('Should update Note 1 title', async () => { + const response = await updateNote({ id: notesId[0], title: 'Note 1 New Title' }, client); + expect(response.data).toBeDefined(); + expect(response.data.updateNote.title).toBe('Note 1 New Title'); +}); + +test('Should create a new Note', async () => { + const response = await createNote(client, { title: 'New note', description: 'New note description' }); + expect(response.data).toBeDefined(); + expect(response.data.createNote).toEqual({ id: response.data.createNote.id, title: 'New note', description: 'New note description' }); +}) + +test('Delete Note 1', async () => { + const response = await deleteNote(client, notesId[1]); + expect(response.data).toBeDefined(); + expect(response.data.deleteNote).toEqual({ id: notesId[1], description: 'Note B Description', title: 'Note B' }); +}); + +async function updateNote(input: any, client: ApolloServerTestClient) { + const response = await client.mutate({ + operationName: "updateNote", + mutation: documents, + variables: { input } + }); + + return response; +} + +async function createNote(client: ApolloServerTestClient, input: any) { + const response = await client.mutate({ + operationName: "createNote", + mutation: documents, variables: { input } + }); + + return response; +} + +async function deleteNote(client: ApolloServerTestClient, id: string | number) { + const response = await client.mutate({ + operationName: "deleteNote", + mutation: documents, + variables: { input: { id } } + }); + + return response; +} + +async function getNote(id: string | number, client: ApolloServerTestClient) { + const response = await client.query({ + operationName: "getNote", + query: documents, + variables: { id } + }); + + return response; +} + +async function findNoteComments(noteId: string, client: ApolloServerTestClient) { + const response = await client.query({ + operationName: "findComments", + query: documents, + variables: { filter: { noteId } } + }); + + return response; +} + diff --git a/integration/tests/runtime-workflow.ts b/integration/tests/runtime-workflow-postgres.ts similarity index 91% rename from integration/tests/runtime-workflow.ts rename to integration/tests/runtime-workflow-postgres.ts index 850e13a74e..901e9c9d99 100644 --- a/integration/tests/runtime-workflow.ts +++ b/integration/tests/runtime-workflow-postgres.ts @@ -10,10 +10,12 @@ import { loadConfig } from 'graphql-config'; import { loadDocuments } from '@graphql-toolkit/core'; import { GraphQLFileLoader } from '@graphql-toolkit/graphql-file-loader'; import * as Knex from 'knex'; -import { GraphbackGenerator, buildGraphbackAPI, GraphbackAPI } from "graphback/src"; +import { buildGraphbackAPI, GraphbackAPI } from "graphback/src"; import { DocumentNode } from 'graphql'; import { migrateDB } from '../../packages/graphql-migrations/src'; -import { createKnexDbProvider } from "../../packages/graphback-runtime-knex/src" +import { createKnexDbProvider } from "../../packages/graphback-runtime-knex"; +import { SchemaCRUDPlugin } from '../../packages/graphback-codegen-schema'; +import { ClientCRUDPlugin } from '../../packages/graphback-codegen-client'; /** global config */ let db: Knex; @@ -23,48 +25,12 @@ let graphbackApi: GraphbackAPI; let documents: DocumentNode; -const modelText = `""" @model """ -type Note { - id: ID! - title: String! - description: String - """ - @oneToMany(field: 'note') - """ - comments: [Comment]! -} - -""" @model """ -type Comment { - id: ID! - text: String - description: String - """ - @oneToOne - """ - metadata: CommentMetadata -} - -""" -@model -""" -type CommentMetadata { - id: ID! - opened: Boolean -} - -type Query { - helloWorld: String -}` +const modelText = readFileSync('./mock.graphql').toString(); beforeAll(async () => { try { - const { graphbackConfig } = await getConfig(); - mkdirSync("./output"); - mkdirSync("./output/client") - const generator = new GraphbackGenerator(modelText, graphbackConfig); - generator.generateSourceCode(); - + mkdirSync("./output-postgres"); + mkdirSync("./output-postgres/client") const dbMigrationsConfig = { client: "pg", connection: { @@ -78,7 +44,11 @@ beforeAll(async () => { db = Knex(dbMigrationsConfig); graphbackApi = buildGraphbackAPI(modelText, { - dataProviderCreator: createKnexDbProvider(db) + dataProviderCreator: createKnexDbProvider(db), + plugins: [ + new SchemaCRUDPlugin({outputPath: "./output-postgres/schema/schema.graphql"}), + new ClientCRUDPlugin({format: 'graphql', outputFile: './output-postgres/client/graphback.graphql'}) + ] }); const { newDB } = await migrateDB(dbMigrationsConfig, graphbackApi.schema); @@ -87,7 +57,7 @@ beforeAll(async () => { expect(newDB).toMatchSnapshot(); - const source = await loadDocuments(path.resolve(`./output/client/**/*.graphql`), { + const source = await loadDocuments(path.resolve(`./output-postgres/client/**/*.graphql`), { loaders: [ new GraphQLFileLoader() ] @@ -112,9 +82,9 @@ beforeEach(() => { afterEach(() => server.stop()) -afterAll(() => { - rmdirSync(path.resolve('./output'), { recursive: true }); - +afterAll(async () => { + rmdirSync(path.resolve('./output-postgres'), { recursive: true }); + await db.schema.dropTableIfExists('comment').dropTableIfExists('commentmetadata').dropTableIfExists('note'); return db.destroy(); }); diff --git a/packages/graphback-core/src/plugin/GraphbackPluginEngine.ts b/packages/graphback-core/src/plugin/GraphbackPluginEngine.ts index 95073c3311..5cc1a21212 100644 --- a/packages/graphback-core/src/plugin/GraphbackPluginEngine.ts +++ b/packages/graphback-core/src/plugin/GraphbackPluginEngine.ts @@ -58,7 +58,7 @@ export class GraphbackPluginEngine { public createSchema(): GraphbackCoreMetadata { if (this.plugins.length === 0) { - throw new Error("GraphbackEngine: No Graphback plugins registered") + console.warn("GraphbackEngine: No Graphback plugins registered"); } //We need to apply all required changes to the schema we need //This is to ensure that every plugin can add changes to the schema