Skip to content

Commit

Permalink
Allow to configure persisted operations plugin (#6505)
Browse files Browse the repository at this point in the history
* Add example and tests for persisted queries

* add test for file documents

* add tests to verify we can still run arbitrary queries

* allow to configure persisted queries

* add documentation

* add persisted operation example to build list of integration test

* changeset

* Fix changeset version
  • Loading branch information
EmrysMyrddin authored Feb 7, 2024
1 parent 12ba8f2 commit ae7b085
Show file tree
Hide file tree
Showing 14 changed files with 338 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/cuddly-islands-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphql-mesh/config": patch
"@graphql-mesh/types": patch
---

Allow to configure persisted operations behaviour
26 changes: 26 additions & 0 deletions examples/persisted-operations/.meshrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
sources:
- name: Hello World
handler:
jsonSchema:
operations:
- type: Query
field: greeting
method: GET
path: /
responseSample:
hello: world
plugins:
- mock:
mocks:
- apply: Query.greeting
documents:
# Documents can be specified by filename or as a glob pattern
- ./src/**/*.graphql
# Or by inline definition
- |
query TypeName {
__typename
}
persistedOperations:
allowArbitraryOperations: true
5 changes: 5 additions & 0 deletions examples/persisted-operations/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
21 changes: 21 additions & 0 deletions examples/persisted-operations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "example-persisted-operations",
"version": "0.0.1",
"license": "MIT",
"private": true,
"scripts": {
"build": "mesh build",
"start": "mesh dev",
"test": "mesh build && jest"
},
"dependencies": {
"@graphql-mesh/cli": "0.88.5",
"@graphql-mesh/json-schema": "0.97.4",
"@graphql-mesh/plugin-mock": "0.96.3",
"@graphql-yoga/plugin-sofa": "3.1.1",
"graphql": "16.8.1"
},
"devDependencies": {
"jest": "29.7.0"
}
}
8 changes: 8 additions & 0 deletions examples/persisted-operations/sandbox.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"template": "node",
"container": {
"node": "21"
}
}
5 changes: 5 additions & 0 deletions examples/persisted-operations/src/hello-world.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query HelloWorld {
greeting {
hello
}
}
96 changes: 96 additions & 0 deletions examples/persisted-operations/tests/persisted-queries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { join } from 'path';
import { ExecutionResult } from 'graphql';
import { findAndParseConfig } from '@graphql-mesh/cli';
import { createMeshHTTPHandler, MeshHTTPHandler } from '@graphql-mesh/http';
import { getMesh, MeshInstance } from '@graphql-mesh/runtime';

const baseDir = join(__dirname, '..');

const meshInstances = {
'Mesh runtime': async () => {
const config = await findAndParseConfig({ dir: baseDir });
return getMesh(config);
},
'Mesh artifact': async () => {
const { getBuiltMesh } = await import('../.mesh/index');
return getBuiltMesh();
},
};

describe('Persisted Queries', () => {
describe.each(Object.entries(meshInstances))('%s', (_, getMeshInstance) => {
let mesh: MeshInstance;
let meshHttp: MeshHTTPHandler;

beforeAll(async () => {
mesh = await getMeshInstance();
meshHttp = createMeshHTTPHandler({
baseDir,
getBuiltMesh: () => Promise.resolve(mesh),
});
});

afterAll(() => mesh.destroy());

it('should give correct response for inline persisted operation', async () => {
const response = await meshHttp.fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
extensions: {
persistedQuery: {
version: 1,
sha256Hash: 'ece829f774dcb3e1358987feb1f86832b39472406a3ef65dce6a2a740304148a',
},
},
}),
});

expect(response.status).toBe(200);
const result = (await response.json()) as ExecutionResult;
expect(result?.errors).toBeFalsy();
expect(result.data).toEqual({ __typename: 'Query' });
});

it('should give correct response for file documents', async () => {
const response = await meshHttp.fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
extensions: {
persistedQuery: {
version: 1,
sha256Hash: '2e0534aab6b2b83bc791439094830b45b621deec2087b8567539f37defd391ac',
},
},
}),
});

expect(response.status).toBe(200);
const result = (await response.json()) as ExecutionResult;
expect(result?.errors).toBeFalsy();
expect(result.data).toEqual({ greeting: { hello: 'world' } });
});

it('should not restrict to persisted queries only', async () => {
const response = await meshHttp.fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: /* GraphQL */ `
query HelloWorld {
greeting {
__typename
}
}
`,
}),
});

expect(response.status).toBe(200);
const result = (await response.json()) as ExecutionResult;
expect(result?.errors).toBeFalsy();
expect(result.data).toEqual({ greeting: { __typename: 'query_greeting' } });
});
});
});
21 changes: 21 additions & 0 deletions newrelic_agent.log
Original file line number Diff line number Diff line change
Expand Up @@ -1419,3 +1419,24 @@
{"v":0,"level":40,"name":"newrelic","hostname":"ARDAL","pid":51523,"time":"2023-09-14T19:01:30.698Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"zlib"}
{"v":0,"level":50,"name":"newrelic","hostname":"ARDAL","pid":51523,"time":"2023-09-14T19:01:30.830Z","msg":"Unable to create segment for external request: External/localhost:3000/graphql","component":"Envelop_NewRelic_Plugin","module":"Test Agent"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":51523,"time":"2023-09-14T19:01:30.887Z","msg":"Envelop_NewRelic_Plugin registered","component":"Envelop_NewRelic_Plugin","module":"Test Agent"}
{"v":0,"level":30,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.159Z","msg":"Unable to find configuration file. If a configuration file is desired (common for non-containerized environments), a base configuration file can be copied from /Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/newrelic/newrelic.js and renamed to \"newrelic.js\" in the directory from which you will start your application. Attempting to start agent using environment variables."}
{"v":0,"level":30,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.495Z","msg":"Using New Relic for Node.js. Agent version: 10.6.2; Node version: v20.10.0."}
{"v":0,"level":30,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.499Z","msg":"Using LegacyContextManager"}
{"v":0,"level":50,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.500Z","msg":"New Relic for Node.js was unable to bootstrap itself due to an error:","stack":"Error: New Relic requires that you name this application!\nSet app_name in your newrelic.js or newrelic.cjs file or set environment variable\nNEW_RELIC_APP_NAME. Not starting!\n at createAgent (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/newrelic/index.js:149:11)\n at initialize (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/newrelic/index.js:86:15)\n at Object.<anonymous> (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/newrelic/index.js:37:3)\n at Runtime._execModule (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/jest-config/node_modules/jest-runtime/build/index.js:1439:24)\n at Runtime._loadModule (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/jest-config/node_modules/jest-runtime/build/index.js:1022:12)\n at Runtime.requireModule (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/jest-config/node_modules/jest-runtime/build/index.js:882:12)\n at Runtime.requireModuleOrMock (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/jest-config/node_modules/jest-runtime/build/index.js:1048:21)\n at Object.require (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/packages/plugins/newrelic/src/index.ts:3:1)\n at Runtime._execModule (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/jest-config/node_modules/jest-runtime/build/index.js:1439:24)\n at Runtime._loadModule (/Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/jest-config/node_modules/jest-runtime/build/index.js:1022:12)","message":"New Relic requires that you name this application!\nSet app_name in your newrelic.js or newrelic.cjs file or set environment variable\nNEW_RELIC_APP_NAME. Not starting!"}
{"v":0,"level":30,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.521Z","msg":"Unable to find configuration file. If a configuration file is desired (common for non-containerized environments), a base configuration file can be copied from /Users/valentin/Dev/Projects/TheGuild/graphql-mesh/node_modules/newrelic/newrelic.js and renamed to \"newrelic.js\" in the directory from which you will start your application. Attempting to start agent using environment variables."}
{"v":0,"level":30,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.522Z","msg":"Using LegacyContextManager"}
{"v":0,"level":30,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.522Z","msg":"Agent state changed from stopped to started."}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.522Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"globals"}
{"v":0,"level":30,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.524Z","msg":"Adding destroy hook to clean up unresolved promises.","component":"async_hooks"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.524Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"child_process"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.525Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"crypto"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.526Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"dns"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.526Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"fs"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.527Z","msg":"Enabling debug mode for shim!","component":"TransactionShim","module":"http"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.529Z","msg":"Enabling debug mode for shim!","component":"TransactionShim","module":"https"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.529Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"inspector"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.530Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"net"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.530Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"timers"}
{"v":0,"level":40,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.531Z","msg":"Enabling debug mode for shim!","component":"Shim","module":"zlib"}
{"v":0,"level":50,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.558Z","msg":"Unable to create segment for external request: External/localhost:3000/graphql","component":"Envelop_NewRelic_Plugin","module":"Test Agent"}
{"v":0,"level":30,"name":"newrelic","hostname":"Pohm.local","pid":7751,"time":"2024-01-26T13:59:42.663Z","msg":"Envelop_NewRelic_Plugin registered","component":"Envelop_NewRelic_Plugin","module":"Test Agent"}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"packageManager": "[email protected]",
"scripts": {
"build": "bob build",
"build-test-artifacts": "yarn workspace json-schema-example build && yarn workspace example-fastify build",
"build-test-artifacts": "yarn workspace json-schema-example build && yarn workspace example-fastify build && yarn workspace example-persisted-operations build",
"build:website": "cd website && yarn build",
"ci:lint": "eslint --output-file eslint_report.json --ext .ts --format json \"./packages/**/src/**/*.ts\"",
"clean": "rm -rf packages/**/dist packages/**/**/dist examples/**/node_modules/.bin/*mesh* .bob",
Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ export async function processConfig(
},
skipDocumentValidation: true,
allowArbitraryOperations: true,
...config.persistedOperations,
}),
);
if (options.generateCode) {
Expand All @@ -653,6 +654,7 @@ export async function processConfig(
getPersistedOperation(key) {
return documentHashMap[key];
},
...${JSON.stringify(config.persistedOperations ?? {}, null, 2)}
}))`);
}
}
Expand Down
35 changes: 35 additions & 0 deletions packages/config/yaml-config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ type Query {
"""
documents: [String!]
"""
Configure persisted operations options
"""
persistedOperations: PersistedOperationsConfig
"""
Logger instance that matches `Console` interface of NodeJS
"""
logger: Any
Expand Down Expand Up @@ -151,3 +155,34 @@ type PubSubConfig {
name: String!
config: Any
}

type PersistedOperationsConfig {
"""
Whether to allow execution of arbitrary GraphQL operations aside from persisted operations.
"""
allowArbitraryOperations: Boolean
"""
Whether to skip validation of the persisted operation
"""
skipDocumentValidation: Boolean

"""
Custom errors to be thrown
"""
customErrors: CustomPersistedQueryErrors
}

type CustomPersistedQueryErrors {
"""
Error to be thrown when the persisted operation is not found
"""
notFound: String
"""
Error to be thrown when rejecting non-persisted operations
"""
persistedQueryOnly: String
"""
Error to be thrown when the extraction of the persisted operation id failed
"""
keyNotFound: String
}
42 changes: 42 additions & 0 deletions packages/types/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,44 @@
},
"required": ["name"]
},
"PersistedOperationsConfig": {
"additionalProperties": false,
"type": "object",
"title": "PersistedOperationsConfig",
"properties": {
"allowArbitraryOperations": {
"type": "boolean",
"description": "Whether to allow execution of arbitrary GraphQL operations aside from persisted operations."
},
"skipDocumentValidation": {
"type": "boolean",
"description": "Whether to skip validation of the persisted operation"
},
"customErrors": {
"$ref": "#/definitions/CustomPersistedQueryErrors",
"description": "Custom errors to be thrown"
}
}
},
"CustomPersistedQueryErrors": {
"additionalProperties": false,
"type": "object",
"title": "CustomPersistedQueryErrors",
"properties": {
"notFound": {
"type": "string",
"description": "Error to be thrown when the persisted operation is not found"
},
"persistedQueryOnly": {
"type": "string",
"description": "Error to be thrown when rejecting non-persisted operations"
},
"keyNotFound": {
"type": "string",
"description": "Error to be thrown when the extraction of the persisted operation id failed"
}
}
},
"GraphQLHandlerMultipleHTTPConfiguration": {
"additionalProperties": false,
"type": "object",
Expand Down Expand Up @@ -4211,6 +4249,10 @@
"additionalItems": false,
"description": "Provide a query or queries for GraphQL Playground, validation and SDK Generation\nThe value can be the file path, glob expression for the file paths or the SDL.\n(.js, .jsx, .graphql, .gql, .ts and .tsx files are supported."
},
"persistedOperations": {
"$ref": "#/definitions/PersistedOperationsConfig",
"description": "Configure persisted operations options"
},
"logger": {
"anyOf": [
{
Expand Down
32 changes: 32 additions & 0 deletions packages/types/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface Config {
* (.js, .jsx, .graphql, .gql, .ts and .tsx files are supported.
*/
documents?: string[];
persistedOperations?: PersistedOperationsConfig;
/**
* Logger instance that matches `Console` interface of NodeJS
*/
Expand Down Expand Up @@ -1730,6 +1731,37 @@ export interface PubSubConfig {
name: string;
config?: any;
}
/**
* Configure persisted operations options
*/
export interface PersistedOperationsConfig {
/**
* Whether to allow execution of arbitrary GraphQL operations aside from persisted operations.
*/
allowArbitraryOperations?: boolean;
/**
* Whether to skip validation of the persisted operation
*/
skipDocumentValidation?: boolean;
customErrors?: CustomPersistedQueryErrors;
}
/**
* Custom errors to be thrown
*/
export interface CustomPersistedQueryErrors {
/**
* Error to be thrown when the persisted operation is not found
*/
notFound?: string;
/**
* Error to be thrown when rejecting non-persisted operations
*/
persistedQueryOnly?: string;
/**
* Error to be thrown when the extraction of the persisted operation id failed
*/
keyNotFound?: string;
}
export interface Plugin {
maskedErrors?: MaskedErrorsPluginConfig;
immediateIntrospection?: any;
Expand Down
Loading

0 comments on commit ae7b085

Please sign in to comment.