@axinom/mosaic-graphql-common 0.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/common/checks.d.ts +9 -0
- package/dist/common/checks.d.ts.map +1 -0
- package/dist/common/checks.js +21 -0
- package/dist/common/checks.js.map +1 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +19 -0
- package/dist/common/index.js.map +1 -0
- package/dist/common/types.d.ts +13 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +15 -0
- package/dist/common/types.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/add-error-codes-enum-plugin.d.ts +27 -0
- package/dist/plugins/add-error-codes-enum-plugin.d.ts.map +1 -0
- package/dist/plugins/add-error-codes-enum-plugin.js +78 -0
- package/dist/plugins/add-error-codes-enum-plugin.js.map +1 -0
- package/dist/plugins/annotate-types-with-permissions-plugin.d.ts +22 -0
- package/dist/plugins/annotate-types-with-permissions-plugin.d.ts.map +1 -0
- package/dist/plugins/annotate-types-with-permissions-plugin.js +145 -0
- package/dist/plugins/annotate-types-with-permissions-plugin.js.map +1 -0
- package/dist/plugins/deprecate-stray-node-id-fields-plugin.d.ts +14 -0
- package/dist/plugins/deprecate-stray-node-id-fields-plugin.d.ts.map +1 -0
- package/dist/plugins/deprecate-stray-node-id-fields-plugin.js +37 -0
- package/dist/plugins/deprecate-stray-node-id-fields-plugin.js.map +1 -0
- package/dist/plugins/generic-bulk-plugin-factory.d.ts +49 -0
- package/dist/plugins/generic-bulk-plugin-factory.d.ts.map +1 -0
- package/dist/plugins/generic-bulk-plugin-factory.js +181 -0
- package/dist/plugins/generic-bulk-plugin-factory.js.map +1 -0
- package/dist/plugins/graphiql-management-mode-plugin-hook.d.ts +13 -0
- package/dist/plugins/graphiql-management-mode-plugin-hook.d.ts.map +1 -0
- package/dist/plugins/graphiql-management-mode-plugin-hook.js +44 -0
- package/dist/plugins/graphiql-management-mode-plugin-hook.js.map +1 -0
- package/dist/plugins/index.d.ts +10 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +26 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/omit-from-query-root-plugin.d.ts +17 -0
- package/dist/plugins/omit-from-query-root-plugin.d.ts.map +1 -0
- package/dist/plugins/omit-from-query-root-plugin.js +43 -0
- package/dist/plugins/omit-from-query-root-plugin.js.map +1 -0
- package/dist/plugins/operations-enum-generator-plugin-factory.d.ts +15 -0
- package/dist/plugins/operations-enum-generator-plugin-factory.d.ts.map +1 -0
- package/dist/plugins/operations-enum-generator-plugin-factory.js +108 -0
- package/dist/plugins/operations-enum-generator-plugin-factory.js.map +1 -0
- package/dist/plugins/subscriptions-plugin-factory.d.ts +9 -0
- package/dist/plugins/subscriptions-plugin-factory.d.ts.map +1 -0
- package/dist/plugins/subscriptions-plugin-factory.js +67 -0
- package/dist/plugins/subscriptions-plugin-factory.js.map +1 -0
- package/dist/plugins/validation-directives-plugin.d.ts +6 -0
- package/dist/plugins/validation-directives-plugin.d.ts.map +1 -0
- package/dist/plugins/validation-directives-plugin.js +117 -0
- package/dist/plugins/validation-directives-plugin.js.map +1 -0
- package/dist/postgraphile/enhance-graphql-errors.d.ts +48 -0
- package/dist/postgraphile/enhance-graphql-errors.d.ts.map +1 -0
- package/dist/postgraphile/enhance-graphql-errors.js +67 -0
- package/dist/postgraphile/enhance-graphql-errors.js.map +1 -0
- package/dist/postgraphile/index.d.ts +4 -0
- package/dist/postgraphile/index.d.ts.map +1 -0
- package/dist/postgraphile/index.js +20 -0
- package/dist/postgraphile/index.js.map +1 -0
- package/dist/postgraphile/postgraphile-options-builder.d.ts +273 -0
- package/dist/postgraphile/postgraphile-options-builder.d.ts.map +1 -0
- package/dist/postgraphile/postgraphile-options-builder.js +419 -0
- package/dist/postgraphile/postgraphile-options-builder.js.map +1 -0
- package/dist/postgraphile/websocket-utils.d.ts +11 -0
- package/dist/postgraphile/websocket-utils.d.ts.map +1 -0
- package/dist/postgraphile/websocket-utils.js +17 -0
- package/dist/postgraphile/websocket-utils.js.map +1 -0
- package/package.json +61 -0
- package/src/common/checks.ts +23 -0
- package/src/common/index.ts +2 -0
- package/src/common/types.ts +15 -0
- package/src/index.ts +3 -0
- package/src/plugins/add-error-codes-enum-plugin.ts +102 -0
- package/src/plugins/annotate-types-with-permissions-plugin.spec.ts +158 -0
- package/src/plugins/annotate-types-with-permissions-plugin.ts +205 -0
- package/src/plugins/deprecate-stray-node-id-fields-plugin.ts +41 -0
- package/src/plugins/generic-bulk-plugin-factory.ts +313 -0
- package/src/plugins/graphiql-management-mode-plugin-hook.ts +46 -0
- package/src/plugins/index.ts +9 -0
- package/src/plugins/omit-from-query-root-plugin.ts +69 -0
- package/src/plugins/operations-enum-generator-plugin-factory.ts +130 -0
- package/src/plugins/subscriptions-plugin-factory.ts +114 -0
- package/src/plugins/validation-directives-plugin.ts +141 -0
- package/src/postgraphile/enhance-graphql-errors.spec.ts +241 -0
- package/src/postgraphile/enhance-graphql-errors.ts +138 -0
- package/src/postgraphile/index.ts +3 -0
- package/src/postgraphile/postgraphile-options-builder.spec.ts +744 -0
- package/src/postgraphile/postgraphile-options-builder.ts +510 -0
- package/src/postgraphile/websocket-utils.ts +19 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { camelCase, Plugin, SchemaBuilder } from 'graphile-build';
|
|
2
|
+
import inflection from 'inflection';
|
|
3
|
+
import { Dict } from '../common';
|
|
4
|
+
|
|
5
|
+
interface Constraint {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
tableId: number;
|
|
9
|
+
tableIndexes: [number];
|
|
10
|
+
definition: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Constraints {
|
|
14
|
+
[Key: string]: Constraint[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
async function loadDbConstraints(builder: SchemaBuilder): Promise<any[]> {
|
|
19
|
+
const constraintQuery = `
|
|
20
|
+
SELECT
|
|
21
|
+
oid, conname, conrelid, conkey, pg_get_constraintdef(oid) AS definition
|
|
22
|
+
FROM
|
|
23
|
+
pg_constraint
|
|
24
|
+
WHERE
|
|
25
|
+
contype = 'c' AND conrelid > 0;`;
|
|
26
|
+
|
|
27
|
+
// this pgConfig is not part of the public TS builder API so it may change
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
const queryResult = await (builder as any).options.pgConfig.query(
|
|
30
|
+
constraintQuery,
|
|
31
|
+
);
|
|
32
|
+
return queryResult.rows;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getConstraints(builder: SchemaBuilder): Promise<Constraints> {
|
|
36
|
+
const constraints: Constraints = {};
|
|
37
|
+
const rows = await loadDbConstraints(builder);
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
const key = `${row.conrelid}`;
|
|
40
|
+
constraints[key] = constraints[key] || [];
|
|
41
|
+
constraints[key].push({
|
|
42
|
+
id: row.oid,
|
|
43
|
+
name: row.conname,
|
|
44
|
+
tableId: row.conrelid,
|
|
45
|
+
definition: row.definition,
|
|
46
|
+
tableIndexes: row.conkey,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return constraints;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mapCheckToValidation(constraint: string): string {
|
|
53
|
+
const regex =
|
|
54
|
+
/CHECK\s*\(\s*([a-zA-Z0-9_]+\.){0,1}([a-zA-Z0-9_]+)\s*\(\s*(['a-zA-Z0-9_]+)\s*,\s*(['a-zA-Z0-9_]+)/gm;
|
|
55
|
+
|
|
56
|
+
const result = regex.exec(constraint);
|
|
57
|
+
if (result?.length && result.length > 4) {
|
|
58
|
+
switch (result[2]) {
|
|
59
|
+
// custom handle when parameters are used - otherwise default is fine
|
|
60
|
+
case 'constraint_max_length':
|
|
61
|
+
return `@maxLength(${result[4]})`;
|
|
62
|
+
case 'constraint_min_length':
|
|
63
|
+
return `@minLength(${result[4]})`;
|
|
64
|
+
default:
|
|
65
|
+
// eslint-disable-next-line no-case-declarations
|
|
66
|
+
const constraintName = inflection.camelize(
|
|
67
|
+
result[2].replace('constraint_', ''),
|
|
68
|
+
true,
|
|
69
|
+
);
|
|
70
|
+
return `@${constraintName}()`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Plugin that adds validation directives to graphql schema descriptions.
|
|
78
|
+
*/
|
|
79
|
+
export const ValidationDirectivesPlugin: Plugin = async (builder) => {
|
|
80
|
+
const constraints = await getConstraints(builder);
|
|
81
|
+
|
|
82
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, _build, context) => {
|
|
83
|
+
const tableConstraints =
|
|
84
|
+
constraints[`${context.scope.pgIntrospection?.id}`];
|
|
85
|
+
const columnMappings: Dict<{ annotations: string[] }> = {};
|
|
86
|
+
// check if there are any constraints in the DB
|
|
87
|
+
if (tableConstraints && Object.keys(fields).length) {
|
|
88
|
+
for (const constraint of tableConstraints) {
|
|
89
|
+
for (const tableIndex of constraint.tableIndexes) {
|
|
90
|
+
// find the DB column by its index with 'pg_constraint.conkey'
|
|
91
|
+
const column = context.scope.pgIntrospection.attributes.find(
|
|
92
|
+
(at: { num: number }) => at.num === tableIndex,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const validation = mapCheckToValidation(constraint.definition);
|
|
96
|
+
if (validation) {
|
|
97
|
+
// Converting the column name to camelCase to follow the general GraphQL naming pattern when adding it to the description.
|
|
98
|
+
const prop = camelCase(column.name);
|
|
99
|
+
columnMappings[prop] = columnMappings[prop] ?? { annotations: [] };
|
|
100
|
+
columnMappings[prop].annotations.push(validation);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const prop in columnMappings) {
|
|
107
|
+
const f = fields[prop];
|
|
108
|
+
if (f) {
|
|
109
|
+
const annotations = columnMappings[prop].annotations.sort().join('\n');
|
|
110
|
+
f.description = f.description
|
|
111
|
+
? `${f.description}\n${annotations}`
|
|
112
|
+
: annotations;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return fields;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
builder.hook('GraphQLSchema', (schemaConfig /*, build*/) => {
|
|
120
|
+
return schemaConfig;
|
|
121
|
+
// The following would allow to add custom directives.
|
|
122
|
+
// They can only be used on the server! They are not sent back to clients.
|
|
123
|
+
// return {
|
|
124
|
+
// ...schemaConfig,
|
|
125
|
+
// directives: [
|
|
126
|
+
// ...(schemaConfig.directives || []),
|
|
127
|
+
// build.newWithHooks(
|
|
128
|
+
// build.graphql.GraphQLDirective,
|
|
129
|
+
// {
|
|
130
|
+
// name: 'upper',
|
|
131
|
+
// locations: ['FIELD_DEFINITION', 'FIELD'],
|
|
132
|
+
// args: {
|
|
133
|
+
// name: { type: build.graphql.GraphQLString },
|
|
134
|
+
// },
|
|
135
|
+
// },
|
|
136
|
+
// { isUpperDirective: true },
|
|
137
|
+
// ),
|
|
138
|
+
// ],
|
|
139
|
+
// };
|
|
140
|
+
});
|
|
141
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { GraphQLError, Source } from 'graphql';
|
|
2
|
+
import 'jest-extended';
|
|
3
|
+
import {
|
|
4
|
+
GeneralGraphQlErrorCode,
|
|
5
|
+
GraphQlOperation,
|
|
6
|
+
enhanceGraphqlErrors,
|
|
7
|
+
} from './enhance-graphql-errors';
|
|
8
|
+
|
|
9
|
+
const iso8601Regex =
|
|
10
|
+
/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([0-1][0-9]|2[0-3])(:[0-5][0-9]){2}(\.[0-9]{0,6})?(Z|\+00:00)$/;
|
|
11
|
+
const UNIT_TEST_ERROR = 'UNIT_TEST_ERROR';
|
|
12
|
+
|
|
13
|
+
describe('graphqlErrorHandler', () => {
|
|
14
|
+
let timestampBeforeTest: Date;
|
|
15
|
+
const mockLog = jest.fn();
|
|
16
|
+
const dateAfterTestStart = (received: string | Date): void => {
|
|
17
|
+
const parsedDate = new Date(received).getTime();
|
|
18
|
+
expect(parsedDate).toBeGreaterThanOrEqual(timestampBeforeTest.getTime());
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
const date = new Date();
|
|
23
|
+
date.setSeconds(date.getSeconds() - 10);
|
|
24
|
+
timestampBeforeTest = date;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
jest.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('minimal internal server error -> valid response', async () => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const gqlError = new GraphQLError(
|
|
34
|
+
'test',
|
|
35
|
+
undefined,
|
|
36
|
+
null,
|
|
37
|
+
null,
|
|
38
|
+
null,
|
|
39
|
+
new Error('original'),
|
|
40
|
+
{
|
|
41
|
+
code: UNIT_TEST_ERROR,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
const enhancedErrors = enhanceGraphqlErrors([gqlError]);
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(enhancedErrors).toHaveLength(1);
|
|
50
|
+
const enhancedError = enhancedErrors[0];
|
|
51
|
+
expect(enhancedError.timestamp).toMatch(iso8601Regex);
|
|
52
|
+
dateAfterTestStart(enhancedError.timestamp);
|
|
53
|
+
expect(enhancedError).toEqual({
|
|
54
|
+
timestamp: enhancedError.timestamp,
|
|
55
|
+
message: 'test',
|
|
56
|
+
details: undefined,
|
|
57
|
+
code: GeneralGraphQlErrorCode,
|
|
58
|
+
extensions: {
|
|
59
|
+
exception: {
|
|
60
|
+
code: GeneralGraphQlErrorCode,
|
|
61
|
+
},
|
|
62
|
+
operationName: undefined,
|
|
63
|
+
timestamp: expect.any(String),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
expect(Object.keys(enhancedError)).toHaveLength(5);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('multiple minimal errors -> valid response', async () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
const gqlError = new GraphQLError(
|
|
72
|
+
'test',
|
|
73
|
+
undefined,
|
|
74
|
+
null,
|
|
75
|
+
null,
|
|
76
|
+
null,
|
|
77
|
+
new Error('original'),
|
|
78
|
+
null,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const enhancedErrors = enhanceGraphqlErrors([gqlError, gqlError]);
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(enhancedErrors).toHaveLength(2);
|
|
86
|
+
dateAfterTestStart(enhancedErrors[0].timestamp);
|
|
87
|
+
dateAfterTestStart(enhancedErrors[1].timestamp);
|
|
88
|
+
expect(enhancedErrors[0].code).toBe(GeneralGraphQlErrorCode);
|
|
89
|
+
expect(enhancedErrors[1].code).toBe(GeneralGraphQlErrorCode);
|
|
90
|
+
expect(enhancedErrors[0].message).toBe('test');
|
|
91
|
+
expect(enhancedErrors[1].message).toBe('test');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('enhances and logs GraphQL errors', () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
const gqlErrors = [
|
|
97
|
+
new GraphQLError(
|
|
98
|
+
'test',
|
|
99
|
+
undefined,
|
|
100
|
+
null,
|
|
101
|
+
null,
|
|
102
|
+
null,
|
|
103
|
+
new Error('original'),
|
|
104
|
+
{
|
|
105
|
+
code: UNIT_TEST_ERROR,
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// Act
|
|
111
|
+
const enhancedErrors = enhanceGraphqlErrors(gqlErrors, undefined, mockLog);
|
|
112
|
+
|
|
113
|
+
// Assert
|
|
114
|
+
expect(enhancedErrors).toHaveLength(1);
|
|
115
|
+
expect(mockLog).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('enhances GraphQL errors with additional error mapper', () => {
|
|
119
|
+
// Arrange
|
|
120
|
+
const gqlErrors = [
|
|
121
|
+
new GraphQLError(
|
|
122
|
+
'test',
|
|
123
|
+
undefined,
|
|
124
|
+
null,
|
|
125
|
+
null,
|
|
126
|
+
null,
|
|
127
|
+
new Error('original'),
|
|
128
|
+
{
|
|
129
|
+
code: UNIT_TEST_ERROR,
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
];
|
|
133
|
+
const errorMapper = (error: GraphQLError) => ({
|
|
134
|
+
...error,
|
|
135
|
+
code: 'CUSTOM_ERROR_CODE',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Act
|
|
139
|
+
const enhancedErrors = enhanceGraphqlErrors(
|
|
140
|
+
gqlErrors,
|
|
141
|
+
'test',
|
|
142
|
+
errorMapper,
|
|
143
|
+
undefined,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
expect(enhancedErrors).toHaveLength(1);
|
|
148
|
+
expect(enhancedErrors[0].code).toBe('CUSTOM_ERROR_CODE');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('contains all original data', () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
const gqlError = new GraphQLError(
|
|
154
|
+
'internal',
|
|
155
|
+
undefined,
|
|
156
|
+
new Source(`mutation DeleteAsset {
|
|
157
|
+
deleteAsset(input: {id: 1}) {
|
|
158
|
+
asset {
|
|
159
|
+
id
|
|
160
|
+
title
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}`),
|
|
164
|
+
[11],
|
|
165
|
+
['deleteAsset'],
|
|
166
|
+
null,
|
|
167
|
+
{
|
|
168
|
+
code: UNIT_TEST_ERROR,
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Act
|
|
173
|
+
const enhancedErrors = enhanceGraphqlErrors(
|
|
174
|
+
[gqlError],
|
|
175
|
+
undefined,
|
|
176
|
+
undefined,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Assert
|
|
180
|
+
expect(enhancedErrors).toHaveLength(1);
|
|
181
|
+
const enhancedError = enhancedErrors[0];
|
|
182
|
+
expect(enhancedError).toEqual({
|
|
183
|
+
timestamp: enhancedError.timestamp,
|
|
184
|
+
locations: [
|
|
185
|
+
{
|
|
186
|
+
column: 12,
|
|
187
|
+
line: 1,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
message: 'internal',
|
|
191
|
+
path: ['deleteAsset'],
|
|
192
|
+
details: undefined,
|
|
193
|
+
code: GeneralGraphQlErrorCode,
|
|
194
|
+
extensions: {
|
|
195
|
+
exception: {
|
|
196
|
+
code: GeneralGraphQlErrorCode,
|
|
197
|
+
},
|
|
198
|
+
operationName: undefined,
|
|
199
|
+
timestamp: expect.any(String),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('maps the request data for logging', () => {
|
|
205
|
+
// Arrange
|
|
206
|
+
const gqlError = new GraphQLError(
|
|
207
|
+
'internal',
|
|
208
|
+
undefined,
|
|
209
|
+
new Source(`mutation MyDeleteAsset {
|
|
210
|
+
deleteAsset(input: {id: 1}) {
|
|
211
|
+
asset {
|
|
212
|
+
id
|
|
213
|
+
title
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}`),
|
|
217
|
+
[11],
|
|
218
|
+
['deleteAsset'],
|
|
219
|
+
null,
|
|
220
|
+
{
|
|
221
|
+
code: UNIT_TEST_ERROR,
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
let requestInfo: GraphQlOperation[] | undefined = undefined;
|
|
225
|
+
|
|
226
|
+
// Act
|
|
227
|
+
enhanceGraphqlErrors([gqlError], undefined, undefined, (_e, r) => {
|
|
228
|
+
requestInfo = r;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Assert
|
|
232
|
+
expect(requestInfo).toBeDefined();
|
|
233
|
+
expect(requestInfo).toEqual([
|
|
234
|
+
{
|
|
235
|
+
name: 'MyDeleteAsset',
|
|
236
|
+
operation: 'mutation',
|
|
237
|
+
rootEndpoints: ['deleteAsset'],
|
|
238
|
+
},
|
|
239
|
+
]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FieldNode,
|
|
3
|
+
GraphQLError,
|
|
4
|
+
OperationDefinitionNode,
|
|
5
|
+
OperationTypeNode,
|
|
6
|
+
Source,
|
|
7
|
+
parse,
|
|
8
|
+
} from 'graphql';
|
|
9
|
+
import { GraphQLErrorExtended } from 'postgraphile';
|
|
10
|
+
import { Dict } from '../common';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A GraphQLErrorExtended object with additional properties.
|
|
14
|
+
* This error is thrown by the GraphQL API and returned to the original requester.
|
|
15
|
+
*/
|
|
16
|
+
export declare type GraphQLErrorEnhanced = GraphQLErrorExtended & {
|
|
17
|
+
extensions: {
|
|
18
|
+
/** The timestamp when the error was raised */
|
|
19
|
+
timestamp: string;
|
|
20
|
+
};
|
|
21
|
+
//TODO: remove backwards compatibility after 01-11-2023
|
|
22
|
+
timestamp: string;
|
|
23
|
+
code: string;
|
|
24
|
+
details?: Dict<unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const GeneralGraphQlErrorCode = 'GENERAL_GRAPHQL_ERROR';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Contains data about the failed GraphQL request without exposing passed variables, since they can contain sensitive values.
|
|
31
|
+
*/
|
|
32
|
+
export interface GraphQlOperation {
|
|
33
|
+
/**
|
|
34
|
+
* Type of operation, e.g. query, mutation or subscription.
|
|
35
|
+
*/
|
|
36
|
+
operationType?: OperationTypeNode;
|
|
37
|
+
/**
|
|
38
|
+
* Name of operation, if defined.
|
|
39
|
+
*/
|
|
40
|
+
operationName?: string;
|
|
41
|
+
/**
|
|
42
|
+
* List of root endpoints, since a single operation can perform multiple queries/mutations.
|
|
43
|
+
*/
|
|
44
|
+
rootEndpoints?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const basicErrorMapper = (
|
|
48
|
+
error: GraphQLError,
|
|
49
|
+
operationName?: string,
|
|
50
|
+
): GraphQLErrorEnhanced => {
|
|
51
|
+
return {
|
|
52
|
+
...error,
|
|
53
|
+
message: error.message ?? 'No error description was provided',
|
|
54
|
+
extensions: {
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
operationName,
|
|
57
|
+
exception: {
|
|
58
|
+
code: GeneralGraphQlErrorCode,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
//TODO: remove backwards compatibility after 01-11-2023
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
code: GeneralGraphQlErrorCode,
|
|
64
|
+
details: undefined,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const getGraphQlOperations = (
|
|
69
|
+
requestBody: string,
|
|
70
|
+
): GraphQlOperation[] | undefined => {
|
|
71
|
+
try {
|
|
72
|
+
const parsedGqlRequest = parse(requestBody);
|
|
73
|
+
return (parsedGqlRequest.definitions as OperationDefinitionNode[]).map(
|
|
74
|
+
(op) => ({
|
|
75
|
+
name: op.name?.value ?? 'NO_OPERATION_NAME',
|
|
76
|
+
operation: op.operation,
|
|
77
|
+
rootEndpoints: op.selectionSet.selections.map(
|
|
78
|
+
(selection) => (selection as FieldNode).name.value,
|
|
79
|
+
),
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
// Unable to parse the GraphQL request string.
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Default graphql error handler. Extracts information to return as an error response (timestamp, message, code and path parameters) and logs error and response information using provided logger.
|
|
90
|
+
*
|
|
91
|
+
* @param errors - Array of original graphql errors
|
|
92
|
+
* @param operationName - The operation name that was executed in case the request contained more than one operation. Found in `req.body.operationName`.
|
|
93
|
+
* @param customizeFields - Override error fields message, exception code, and add extensions fields with your custom values. Extension fields are added to the extensions object and cannot use existing field names.
|
|
94
|
+
* @param log - allows you to log the error with parts of the GraphQL request data
|
|
95
|
+
*/
|
|
96
|
+
export const enhanceGraphqlErrors = (
|
|
97
|
+
errors: readonly GraphQLError[],
|
|
98
|
+
operationName?: string,
|
|
99
|
+
customizeFields?: (
|
|
100
|
+
error: GraphQLError,
|
|
101
|
+
originalError?: Error | null,
|
|
102
|
+
) =>
|
|
103
|
+
| {
|
|
104
|
+
message?: string;
|
|
105
|
+
code?: string;
|
|
106
|
+
extensions?: Dict<unknown>;
|
|
107
|
+
}
|
|
108
|
+
| undefined,
|
|
109
|
+
log?: (error: GraphQLErrorEnhanced, operations?: GraphQlOperation[]) => void,
|
|
110
|
+
): GraphQLErrorEnhanced[] => {
|
|
111
|
+
return errors.map((error) => {
|
|
112
|
+
const enhancedError = basicErrorMapper(error, operationName);
|
|
113
|
+
|
|
114
|
+
if (customizeFields) {
|
|
115
|
+
const custom = customizeFields(error, error.originalError);
|
|
116
|
+
enhancedError.message = custom?.message ?? enhancedError.message;
|
|
117
|
+
enhancedError.extensions = {
|
|
118
|
+
...custom?.extensions,
|
|
119
|
+
...enhancedError.extensions,
|
|
120
|
+
};
|
|
121
|
+
enhancedError.extensions.exception.code =
|
|
122
|
+
custom?.code ?? enhancedError.extensions.exception.code;
|
|
123
|
+
|
|
124
|
+
//TODO: remove backwards compatibility after 01-11-2023
|
|
125
|
+
enhancedError.code = enhancedError.extensions.exception.code;
|
|
126
|
+
enhancedError.details = custom?.extensions;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (log) {
|
|
130
|
+
const originalError = error.originalError as Error & { source: Source };
|
|
131
|
+
const request = error.source?.body || originalError?.source?.body;
|
|
132
|
+
const graphQlOperations = getGraphQlOperations(request);
|
|
133
|
+
log(enhancedError, graphQlOperations);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return enhancedError;
|
|
137
|
+
});
|
|
138
|
+
};
|