Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to configure persisted operations plugin #6505

Merged
merged 8 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/cuddly-islands-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"example-persisted-operations": minor
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to bump the examples version

"@graphql-mesh/config": minor
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These packages are v0 so let's keep them bumping as patch, because minor means breaking for v0

"@graphql-mesh/types": minor
EmrysMyrddin marked this conversation as resolved.
Show resolved Hide resolved
---

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
Loading