diff --git a/benchmark-result-linux-with-server.md b/benchmark-result-linux-with-server.md new file mode 100644 index 0000000..3a96710 --- /dev/null +++ b/benchmark-result-linux-with-server.md @@ -0,0 +1,5 @@ +| Command | Mean [s] | Min [s] | Max [s] | Relative | +|:---|---:|---:|---:|---:| +| `ts-node -T graphql-tools-mocking/benchmark.ts graphql-tools-mocking.graphql` | 2.735 ± 0.071 | 2.657 | 2.890 | 1.00 | +| `ts-node -T graphql-tools-mocking/benchmark.ts ts-deco-fe.federated.graphql` | 3.785 ± 0.052 | 3.708 | 3.888 | 1.38 ± 0.04 | +| `ts-node -T graphql-tools-mocking/benchmark.ts voyager-api.federated.graphql` | 5.238 ± 0.081 | 5.134 | 5.392 | 1.92 ± 0.06 | diff --git a/benchmark-result-linux-without-server.md b/benchmark-result-linux-without-server.md new file mode 100644 index 0000000..955a392 --- /dev/null +++ b/benchmark-result-linux-without-server.md @@ -0,0 +1,5 @@ +| Command | Mean [s] | Min [s] | Max [s] | Relative | +|:---|---:|---:|---:|---:| +| `ts-node -T graphql-tools-mocking/benchmark.ts graphql-tools-mocking.graphql --no-server` | 2.645 ± 0.037 | 2.596 | 2.701 | 1.00 | +| `ts-node -T graphql-tools-mocking/benchmark.ts ts-deco-fe.federated.graphql --no-server` | 3.651 ± 0.060 | 3.584 | 3.749 | 1.38 ± 0.03 | +| `ts-node -T graphql-tools-mocking/benchmark.ts voyager-api.federated.graphql --no-server` | 5.238 ± 0.070 | 5.131 | 5.358 | 1.98 ± 0.04 | diff --git a/benchmark-result-m1-with-server.md b/benchmark-result-m1-with-server.md new file mode 100644 index 0000000..3635b4a --- /dev/null +++ b/benchmark-result-m1-with-server.md @@ -0,0 +1,5 @@ +| Command | Mean [s] | Min [s] | Max [s] | Relative | +|:---|---:|---:|---:|---:| +| `ts-node -T graphql-tools-mocking/benchmark.ts graphql-tools-mocking.graphql` | 1.493 ± 0.068 | 1.443 | 1.672 | 1.00 | +| `ts-node -T graphql-tools-mocking/benchmark.ts ts-deco-fe.federated.graphql` | 2.200 ± 0.082 | 2.127 | 2.356 | 1.47 ± 0.09 | +| `ts-node -T graphql-tools-mocking/benchmark.ts voyager-api.federated.graphql` | 2.813 ± 0.062 | 2.750 | 2.949 | 1.88 ± 0.09 | diff --git a/benchmark-result-m1-without-server.md b/benchmark-result-m1-without-server.md new file mode 100644 index 0000000..f1e6c58 --- /dev/null +++ b/benchmark-result-m1-without-server.md @@ -0,0 +1,5 @@ +| Command | Mean [s] | Min [s] | Max [s] | Relative | +|:---|---:|---:|---:|---:| +| `ts-node -T graphql-tools-mocking/benchmark.ts graphql-tools-mocking.graphql --no-server` | 1.522 ± 0.067 | 1.445 | 1.646 | 1.00 | +| `ts-node -T graphql-tools-mocking/benchmark.ts ts-deco-fe.federated.graphql --no-server` | 2.101 ± 0.031 | 2.067 | 2.160 | 1.38 ± 0.06 | +| `ts-node -T graphql-tools-mocking/benchmark.ts voyager-api.federated.graphql --no-server` | 2.773 ± 0.024 | 2.739 | 2.819 | 1.82 ± 0.08 | diff --git a/experiments/graphql-tools-mocking.md b/experiments/graphql-tools-mocking.md index bc42e6c..146cd21 100644 --- a/experiments/graphql-tools-mocking.md +++ b/experiments/graphql-tools-mocking.md @@ -21,3 +21,35 @@ You should be able to see the result of the query that was executed directly. This demo also includes a running server at http://0.0.0.0:4000/graphql which is running GraphiQL. This allows you to explore the original schema while querying with the same fake data. + +## Benchmark + +Install hyperfine with brew or similar: + +```sh +$ brew install hyperfine +``` + +Copy the schemas that you want to test against to `schemas/` folder. + +Run the tests in your environment and export it to MD file like: + +```sh +$ hyperfine --warmup 3 -r 10 \ + 'ts-node -T graphql-tools-mocking/benchmark.ts graphql-tools-mocking.graphql' \ + 'ts-node -T graphql-tools-mocking/benchmark.ts .graphql' \ + 'ts-node -T graphql-tools-mocking/benchmark.ts .graphql' \ + --export-markdown benchmark-result--.md +``` + +If you want to test without booting the GQL server - currently using +[Yoga](https://www.the-guild.dev/graphql/yoga-server) - use the `--no-server` +option: + +```sh +$ hyperfine --warmup 3 -r 10 \ + 'ts-node -T graphql-tools-mocking/benchmark.ts graphql-tools-mocking.graphql --no-server' \ + 'ts-node -T graphql-tools-mocking/benchmark.ts .graphql --no-server' \ + 'ts-node -T graphql-tools-mocking/benchmark.ts .graphql --no-server' \ + --export-markdown benchmark-result--.md +``` diff --git a/graphql-tools-mocking/benchmark.ts b/graphql-tools-mocking/benchmark.ts new file mode 100644 index 0000000..b0d7598 --- /dev/null +++ b/graphql-tools-mocking/benchmark.ts @@ -0,0 +1,112 @@ +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { addMocksToSchema } from '@graphql-tools/mock'; +import { graphql } from 'graphql'; +import { faker } from '@faker-js/faker'; +import { createServer } from '@graphql-yoga/node'; +import fs from 'fs'; +import path from 'path'; +import sanitize from 'sanitize-filename'; + +const options = { withServer: true }; +const inputs: string[] = []; +process.argv.slice(2).forEach((arg) => { + switch (arg) { + case '--no-server': + options.withServer = false; + break; + default: + inputs.push(arg); + } +}); + +const providedSchemaPath = inputs[0]; + +const providedSchema = fs.readFileSync( + path.join('schemas', sanitize(providedSchemaPath)), + 'utf-8' +); +const extendedSchema = fs + .readFileSync(path.join('schemas', 'graphql-tools-mocking.graphql'), 'utf-8') + // extend query since other schema above has it already defined + .replace('type Query {', 'extend type Query {'); + +const schemaString = ` +${providedSchema} +${extendedSchema} +`; + +// Make a GraphQL schema with no resolvers +const schema = makeExecutableSchema({ typeDefs: schemaString }); + +// Create a new schema with mocks +const schemaWithMocks = addMocksToSchema({ + schema, + mocks: { + Date: () => new Date().toISOString(), + + BookAuthor: () => { + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); + return { + firstName, + lastName, + fullName: faker.name.fullName({ firstName, lastName }), + }; + }, + + Query: { + books: () => { + // always load only 1 for consistent benchmark results + return [...new Array(1)]; + }, + }, + }, +}); + +if (options.withServer) { + // benchmark with optional GQL server + const server = createServer({ + schema: schemaWithMocks, + }); + + server.start().then(() => { + // stop the server after we start for clean exit of the process + server.stop(); + }); +} + +const query = /* GraphQL */ ` + query Book { + book(id: 6) { + id + description + date + author { + id + firstName + lastName + fullName + } + } + + # this limit input is ignored by the mocker + books(limit: 1) { + id + description + date + author { + id + firstName + lastName + fullName + } + } + } +`; + +graphql({ + schema: schemaWithMocks, + source: query, +}).then((result) => { + console.log('Got result %o', result); +}); diff --git a/graphql-tools-mocking/index.ts b/graphql-tools-mocking/index.ts index 4f7ac44..b66bccc 100644 --- a/graphql-tools-mocking/index.ts +++ b/graphql-tools-mocking/index.ts @@ -3,37 +3,16 @@ import { addMocksToSchema } from '@graphql-tools/mock'; import { graphql } from 'graphql'; import { faker } from '@faker-js/faker'; import { createServer } from '@graphql-yoga/node'; +import fs from 'node:fs'; +import path from 'node:path'; // Fill this in with the schema string // This can be based on introspection too: // https://www.graphql-tools.com/docs/mocking#mocking-a-schema-using-introspection -const schemaString = ` - scalar Date - - type Book { - id: ID! - description: String - date: Date - author: BookAuthor - } - - type BookAuthor { - id: ID! - firstName: String - lastName: String - fullName: String - } - - type Query { - book(id: ID!): Book - books(limit: Int, skip: Int, sort_field: String, sort_order: String): [Book] - } - - type Mutation { - createBook(body: String): Book - deleteBook(id: ID!): Book - } -`; +const schemaString = fs.readFileSync( + path.join('schemas', 'graphql-tools-mocking.graphql'), + 'utf-8' +); // Make a GraphQL schema with no resolvers const schema = makeExecutableSchema({ typeDefs: schemaString }); @@ -55,6 +34,13 @@ const schemaWithMocks = addMocksToSchema({ // bla: 'example', }; }, + + Query: { + books: () => { + // we can't get the input args from the query 😞 + return [...new Array(faker.datatype.number({ min: 2, max: 6 }))]; + }, + }, }, }); @@ -78,6 +64,19 @@ const query = /* GraphQL */ ` fullName } } + + # this limit input is ignored by the mocker + books(limit: 1) { + id + description + date + author { + id + firstName + lastName + fullName + } + } } `; diff --git a/package.json b/package.json index 36f6e6e..fc5cb3f 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "graphql": "^16.5.0", "graphql-faker": "^2.0.0-rc.25", "graphql-mocks": "^0.8.4", - "prettier": "^2.7.1" + "prettier": "^2.7.1", + "sanitize-filename": "^1.6.3" }, "dependencies": { "typescript": "^4.8.2" diff --git a/schemas/graphql-tools-mocking.graphql b/schemas/graphql-tools-mocking.graphql new file mode 100644 index 0000000..d16c33d --- /dev/null +++ b/schemas/graphql-tools-mocking.graphql @@ -0,0 +1,25 @@ +scalar Date + +type Book { + id: ID! + description: String + date: Date + author: BookAuthor +} + +type BookAuthor { + id: ID! + firstName: String + lastName: String + fullName: String +} + +type Query { + book(id: ID!): Book + books(limit: Int, skip: Int, sort_field: String, sort_order: String): [Book] +} + +type Mutation { + createBook(body: String): Book + deleteBook(id: ID!): Book +} diff --git a/yarn.lock b/yarn.lock index 71dd217..6e4325b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2975,6 +2975,13 @@ safe-buffer@5.2.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-filename@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" + integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== + dependencies: + truncate-utf8-bytes "^1.0.0" + seedrandom@3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" @@ -3268,6 +3275,13 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + integrity sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ== + dependencies: + utf8-byte-length "^1.0.1" + ts-node@^10.5.0: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -3358,6 +3372,11 @@ update-browserslist-db@^1.0.5: escalade "^3.1.1" picocolors "^1.0.0" +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" + integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"