@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,313 @@
|
|
|
1
|
+
import { PgClass } from 'graphile-build-pg';
|
|
2
|
+
import { gql as gqlExtended } from 'graphile-utils';
|
|
3
|
+
import GraphQL from 'graphql';
|
|
4
|
+
import { Build, Plugin } from 'postgraphile';
|
|
5
|
+
import { assertDictionary } from '../common';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A collection of parameters to decide on which entities and by which criteria bulk operation should be performed.
|
|
9
|
+
*/
|
|
10
|
+
export interface BulkFilter {
|
|
11
|
+
tableName: string;
|
|
12
|
+
primaryKeyName: string;
|
|
13
|
+
primaryKeyType: string;
|
|
14
|
+
filterType: GraphQL.GraphQLInputType;
|
|
15
|
+
filterName: string;
|
|
16
|
+
queryName: string;
|
|
17
|
+
typeName: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Interface for specifying bulk action endpoint name
|
|
22
|
+
*/
|
|
23
|
+
export interface CustomBulkSettings {
|
|
24
|
+
graphQLEndpointName: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Callback that decides for which filters a bulk action is defined
|
|
29
|
+
*/
|
|
30
|
+
export interface DefinePlugin {
|
|
31
|
+
(filter: BulkFilter): CustomBulkSettings | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parameters that GenericBulkPluginFactory will provide to the callback, so that the actual bulk operation can be performed.
|
|
36
|
+
*/
|
|
37
|
+
export interface BulkIdParameters {
|
|
38
|
+
entityIds: number[] | string[];
|
|
39
|
+
entityType: string;
|
|
40
|
+
primaryKeyName: string;
|
|
41
|
+
tableName: string;
|
|
42
|
+
graphQLAdditionalInput: unknown;
|
|
43
|
+
graphQLContext: { [str: string]: unknown };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface BulkFilterMap {
|
|
47
|
+
[key: string]: BulkFilter;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface BulkOperationBuild extends Build {
|
|
51
|
+
availableBulkFilters: BulkFilterMap;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface BulkInputParameters {
|
|
55
|
+
filter: {
|
|
56
|
+
type: GraphQL.GraphQLInputType;
|
|
57
|
+
};
|
|
58
|
+
input?: {
|
|
59
|
+
type: GraphQL.GraphQLInputType;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get all the database IDs from the entities that were selected via the GraphQL filter
|
|
65
|
+
*/
|
|
66
|
+
const getEntityIds = async (
|
|
67
|
+
filter: BulkFilter,
|
|
68
|
+
graphql: typeof GraphQL,
|
|
69
|
+
args: { [argName: string]: unknown },
|
|
70
|
+
context: unknown,
|
|
71
|
+
schema: GraphQL.GraphQLSchema,
|
|
72
|
+
): Promise<number[] | string[]> => {
|
|
73
|
+
if (filter.queryName === undefined) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`No query found for the filter of type '${filter.filterName}'.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const queryResult = await graphql.execute(
|
|
79
|
+
schema,
|
|
80
|
+
gqlExtended`
|
|
81
|
+
query getEntityIds($filter: ${filter.filterName}) {
|
|
82
|
+
entities: ${filter.queryName}(filter: $filter) {
|
|
83
|
+
nodes {
|
|
84
|
+
${filter.primaryKeyName}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`,
|
|
89
|
+
undefined,
|
|
90
|
+
context,
|
|
91
|
+
args,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return queryResult?.data?.entities?.nodes.map((entity: unknown) => {
|
|
95
|
+
assertDictionary(entity);
|
|
96
|
+
return entity[filter.primaryKeyName];
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getEntityTypeFromCollectionType = (
|
|
101
|
+
type: GraphQL.GraphQLOutputType,
|
|
102
|
+
): string | undefined => {
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
const fields = (type as any)._fields();
|
|
105
|
+
let tempType = fields?.nodes?.type;
|
|
106
|
+
if (tempType !== undefined) {
|
|
107
|
+
while (tempType?.ofType) {
|
|
108
|
+
tempType = tempType?.ofType;
|
|
109
|
+
}
|
|
110
|
+
return tempType?.name;
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const setPayloadGqlType = (
|
|
116
|
+
build: Build,
|
|
117
|
+
pkDbType: string,
|
|
118
|
+
): GraphQL.GraphQLType => {
|
|
119
|
+
let pkGqlType: GraphQL.GraphQLType;
|
|
120
|
+
let bulkMutationPayloadName: string;
|
|
121
|
+
switch (pkDbType) {
|
|
122
|
+
case 'uuid': {
|
|
123
|
+
bulkMutationPayloadName = 'BulkMutationUuidPayload';
|
|
124
|
+
pkGqlType = build.getTypeByName('UUID');
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'int4':
|
|
128
|
+
default: {
|
|
129
|
+
bulkMutationPayloadName = 'BulkMutationIntPayload';
|
|
130
|
+
pkGqlType = build.graphql.GraphQLInt;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const existingPayloadType = build.getTypeByName(bulkMutationPayloadName);
|
|
136
|
+
if (existingPayloadType !== undefined) {
|
|
137
|
+
return existingPayloadType;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
build.addType(
|
|
141
|
+
new build.graphql.GraphQLObjectType({
|
|
142
|
+
name: bulkMutationPayloadName,
|
|
143
|
+
description: 'Bulk mutation payload type.',
|
|
144
|
+
fields: () => ({
|
|
145
|
+
affectedIds: {
|
|
146
|
+
type: build.graphql.GraphQLList(pkGqlType),
|
|
147
|
+
},
|
|
148
|
+
totalCount: { type: build.graphql.GraphQLInt },
|
|
149
|
+
}),
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return build.getTypeByName(bulkMutationPayloadName);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Plugin factory to create bulk mutations based on query filters
|
|
158
|
+
* @param callback The callback that shall perform the actual bulk operation. It
|
|
159
|
+
* receives different parameters from the base plugin to assist with the
|
|
160
|
+
* operation, e.g. a list of ids for which the bulk operation shall be performed.
|
|
161
|
+
* @param definePlugin The callback that decides for which filters a bulk action is defined.
|
|
162
|
+
* @param inputType Defines additional input parameters for the bulk action. The values are forwarded as part of the message payload.
|
|
163
|
+
*/
|
|
164
|
+
export const GenericBulkPluginFactory = (
|
|
165
|
+
callback: { (parameters: BulkIdParameters): Promise<void> },
|
|
166
|
+
definePlugin: DefinePlugin,
|
|
167
|
+
inputType?: GraphQL.GraphQLInputObjectType,
|
|
168
|
+
): Plugin => {
|
|
169
|
+
return async (builder) => {
|
|
170
|
+
// initialize the availableBulkFilters to collect all filters usable for bulk mutations
|
|
171
|
+
builder.hook('build', (build) => {
|
|
172
|
+
(build as BulkOperationBuild).availableBulkFilters = {};
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
inputType !== undefined &&
|
|
176
|
+
build.getTypeByName(inputType.name) === undefined
|
|
177
|
+
) {
|
|
178
|
+
build.addType(inputType);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return build;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
builder.hook(
|
|
185
|
+
'GraphQLInputObjectType:fields',
|
|
186
|
+
(
|
|
187
|
+
fields,
|
|
188
|
+
build,
|
|
189
|
+
{ scope: { isPgConnectionFilter, pgIntrospection }, Self },
|
|
190
|
+
) => {
|
|
191
|
+
if (
|
|
192
|
+
isPgConnectionFilter &&
|
|
193
|
+
pgIntrospection?.kind === 'class' &&
|
|
194
|
+
pgIntrospection?.classKind !== 'c' && // c represents composite types which doesn't have primary keys.
|
|
195
|
+
pgIntrospection?.primaryKeyConstraint?.type === 'p' &&
|
|
196
|
+
pgIntrospection?.primaryKeyConstraint.keyAttributes.length === 1
|
|
197
|
+
) {
|
|
198
|
+
const { name, type } =
|
|
199
|
+
pgIntrospection.primaryKeyConstraint.keyAttributes[0];
|
|
200
|
+
const availableBulkFilters = (build as BulkOperationBuild)
|
|
201
|
+
.availableBulkFilters;
|
|
202
|
+
|
|
203
|
+
const table: PgClass = pgIntrospection;
|
|
204
|
+
availableBulkFilters[Self.name] = {
|
|
205
|
+
tableName: table.name,
|
|
206
|
+
primaryKeyName: name,
|
|
207
|
+
primaryKeyType: type.name,
|
|
208
|
+
filterName: Self.name,
|
|
209
|
+
filterType: Self,
|
|
210
|
+
queryName: 'set-later',
|
|
211
|
+
typeName: 'set-later',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return fields;
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Add the query endpoints to the corresponding filters
|
|
219
|
+
// Add the bulk mutations based on what the plugin needs
|
|
220
|
+
builder.hook(
|
|
221
|
+
'GraphQLObjectType:fields',
|
|
222
|
+
(fields, build, { scope: { isRootQuery, isRootMutation } }) => {
|
|
223
|
+
const bulkFilters = build.availableBulkFilters as BulkFilterMap;
|
|
224
|
+
if (isRootQuery) {
|
|
225
|
+
// Root query is called first - gather the query operation names
|
|
226
|
+
for (const queryName in fields) {
|
|
227
|
+
const element = fields[queryName];
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
229
|
+
const filterName = (element?.args?.filter?.type as any)?.name;
|
|
230
|
+
if (
|
|
231
|
+
Object.prototype.hasOwnProperty.call(fields, queryName) &&
|
|
232
|
+
element !== undefined &&
|
|
233
|
+
filterName !== undefined &&
|
|
234
|
+
bulkFilters[filterName] !== undefined
|
|
235
|
+
) {
|
|
236
|
+
// we found a query which is using a connection filter property that is usable for bulk mutations
|
|
237
|
+
const filter = bulkFilters[filterName];
|
|
238
|
+
filter.queryName = queryName;
|
|
239
|
+
filter.typeName =
|
|
240
|
+
getEntityTypeFromCollectionType(element.type) ??
|
|
241
|
+
filterName.slice(0, -1 * 'Filter'.length);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isRootMutation) {
|
|
247
|
+
// Root mutation is called after root query - actually add mutations
|
|
248
|
+
for (const filterName in bulkFilters) {
|
|
249
|
+
const filter = bulkFilters[filterName];
|
|
250
|
+
|
|
251
|
+
const settings = definePlugin(filter);
|
|
252
|
+
|
|
253
|
+
if (settings !== undefined) {
|
|
254
|
+
const args: BulkInputParameters = {
|
|
255
|
+
filter: {
|
|
256
|
+
type: filter.filterType,
|
|
257
|
+
},
|
|
258
|
+
...(inputType !== undefined && {
|
|
259
|
+
input: { type: inputType },
|
|
260
|
+
}),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const payloadType = setPayloadGqlType(
|
|
264
|
+
build,
|
|
265
|
+
filter.primaryKeyType,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const operation = {
|
|
269
|
+
type: payloadType,
|
|
270
|
+
args,
|
|
271
|
+
async resolve(
|
|
272
|
+
_parent: unknown,
|
|
273
|
+
input: { [argName: string]: unknown },
|
|
274
|
+
context: { [str: string]: unknown },
|
|
275
|
+
resolveInfo: GraphQL.GraphQLResolveInfo,
|
|
276
|
+
) {
|
|
277
|
+
const ids = await getEntityIds(
|
|
278
|
+
filter,
|
|
279
|
+
build.graphql,
|
|
280
|
+
input,
|
|
281
|
+
context,
|
|
282
|
+
resolveInfo.schema,
|
|
283
|
+
);
|
|
284
|
+
const { input: additionalInput } = input;
|
|
285
|
+
|
|
286
|
+
await callback({
|
|
287
|
+
entityIds: ids,
|
|
288
|
+
entityType: filter.typeName,
|
|
289
|
+
primaryKeyName: filter.primaryKeyName,
|
|
290
|
+
tableName: filter.tableName,
|
|
291
|
+
graphQLAdditionalInput: additionalInput,
|
|
292
|
+
graphQLContext: context,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
affectedIds: ids,
|
|
297
|
+
totalCount: ids.length,
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
build.extend(fields, {
|
|
303
|
+
[settings.graphQLEndpointName]: operation,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return fields;
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Based on:
|
|
3
|
+
*
|
|
4
|
+
* https://gist.github.com/bkeating/cfe0d5e72bd3f9f1f77e1a2ff2309972
|
|
5
|
+
*
|
|
6
|
+
* with some additions for the PostGraphiQL extension.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PostGraphilePlugin } from 'postgraphile';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This plugin override changes the branding piece of graphiql.
|
|
13
|
+
*/
|
|
14
|
+
export const GraphiqlManagementModePluginHook: PostGraphilePlugin = {
|
|
15
|
+
['postgraphile:graphiql:html'](html: string): string {
|
|
16
|
+
return html.replace(
|
|
17
|
+
'</head>',
|
|
18
|
+
`
|
|
19
|
+
<style>
|
|
20
|
+
/* GraphiQL Management Styles */
|
|
21
|
+
#root .graphiql-container .doc-explorer-title-bar,
|
|
22
|
+
#root .graphiql-container .historyPaneWrap > section,
|
|
23
|
+
#secondary-editor-title {
|
|
24
|
+
background: #aaa;
|
|
25
|
+
}
|
|
26
|
+
#root .graphiql-container .topBar,
|
|
27
|
+
#root .graphiql-container .docExplorerShow {
|
|
28
|
+
background: #888;
|
|
29
|
+
}
|
|
30
|
+
#root .graphiql-container .topBar .toolbar-button,
|
|
31
|
+
#root .graphiql-container .topBar .toolbar-button > svg,
|
|
32
|
+
#root .graphiql-container .topBar .execute-button {
|
|
33
|
+
box-shadow: none;
|
|
34
|
+
background: #555;
|
|
35
|
+
color: #ddd;
|
|
36
|
+
fill: #ddd;
|
|
37
|
+
}
|
|
38
|
+
#root .graphiql-container .topBar .toolbar-button {
|
|
39
|
+
border-radius: 0;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
`,
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './add-error-codes-enum-plugin';
|
|
2
|
+
export * from './annotate-types-with-permissions-plugin';
|
|
3
|
+
export * from './deprecate-stray-node-id-fields-plugin';
|
|
4
|
+
export * from './generic-bulk-plugin-factory';
|
|
5
|
+
export * from './graphiql-management-mode-plugin-hook';
|
|
6
|
+
export * from './omit-from-query-root-plugin';
|
|
7
|
+
export * from './operations-enum-generator-plugin-factory';
|
|
8
|
+
export * from './subscriptions-plugin-factory';
|
|
9
|
+
export * from './validation-directives-plugin';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { PgIntrospectionResultsByKind } from 'graphile-build-pg';
|
|
2
|
+
import { GraphQLFieldConfigMap } from 'graphql';
|
|
3
|
+
import { Inflection, Plugin } from 'postgraphile';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type safe interface for unpacking the relevant bits of hook context.
|
|
7
|
+
*/
|
|
8
|
+
interface HookContext {
|
|
9
|
+
Self: {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
scope: {
|
|
13
|
+
isRootQuery: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Plugin for omitting tables from query root but keeping them still accessible
|
|
19
|
+
* from entities through relations.
|
|
20
|
+
*
|
|
21
|
+
* Add `omitFromQueryRoot: true,` to `tags` object of postgraphile smart tags
|
|
22
|
+
* for table you want to omit.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* This is different from `@omit all many` as the tables are still accessible
|
|
26
|
+
* through relations.
|
|
27
|
+
*
|
|
28
|
+
* References:
|
|
29
|
+
* - https://discord.com/channels/489127045289476126/498852330754801666/578654093963427860
|
|
30
|
+
*/
|
|
31
|
+
export const OmitFromQueryRootPlugin: Plugin = (builder) => {
|
|
32
|
+
builder.hook('GraphQLObjectType:fields', (fields, build, context) => {
|
|
33
|
+
const hookContext: HookContext = context as HookContext;
|
|
34
|
+
const introspection: PgIntrospectionResultsByKind =
|
|
35
|
+
build.pgIntrospectionResultsByKind;
|
|
36
|
+
const inflection: Inflection = build.inflection;
|
|
37
|
+
|
|
38
|
+
if (hookContext.Self.name !== 'Query' || !hookContext.scope.isRootQuery) {
|
|
39
|
+
return fields;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tablesToExclude = introspection.class.filter(
|
|
43
|
+
(cls) => cls.tags.omitFromQueryRoot === true,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Remove both single item query and paginated query.
|
|
47
|
+
const fieldNamesToExclude = ([] as string[]).concat(
|
|
48
|
+
...tablesToExclude.map((cls) => [
|
|
49
|
+
inflection.camelCase(`${inflection._singularizedTableName(cls)}`),
|
|
50
|
+
inflection.camelCase(
|
|
51
|
+
inflection.distinctPluralize(inflection._singularizedTableName(cls)),
|
|
52
|
+
),
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return removeProperties(fields, fieldNamesToExclude);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function removeProperties(
|
|
61
|
+
obj: GraphQLFieldConfigMap<unknown, unknown>,
|
|
62
|
+
keysToRemove: string[],
|
|
63
|
+
): GraphQLFieldConfigMap<unknown, unknown> {
|
|
64
|
+
const result = { ...obj };
|
|
65
|
+
for (const key of keysToRemove) {
|
|
66
|
+
delete result[key];
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import endent from 'endent';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { Plugin } from 'postgraphile';
|
|
5
|
+
|
|
6
|
+
interface OperationsEnumGeneratorOptions {
|
|
7
|
+
/** Target root directory where the source files will be written. */
|
|
8
|
+
outRoot: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// TODO: Generate *all* operations, even those that are omitted with smart tags. We need them to allow subqueries.
|
|
12
|
+
// TODO: Consider adding functionality to group operations also by tables, not only queries/mutations.
|
|
13
|
+
/**
|
|
14
|
+
* PostGraphile plugin factory that will create a plugin for generating operation (queries and mutations) enums for the EnforceStrictPermissionsPlugin.
|
|
15
|
+
* - Make sure to register this after all schema modifying plugins and before EnforceStrictPermissionsPlugin.
|
|
16
|
+
* - This plugin does not modify the schema.
|
|
17
|
+
* - Enums will be generated under `src/generated/security/operations`.
|
|
18
|
+
* @param options - Plugin configuration options.
|
|
19
|
+
*/
|
|
20
|
+
export function OperationsEnumGeneratorPluginFactory(
|
|
21
|
+
options: OperationsEnumGeneratorOptions = {
|
|
22
|
+
outRoot: './src/generated/security/operations',
|
|
23
|
+
},
|
|
24
|
+
): Plugin {
|
|
25
|
+
return (builder, _options) => {
|
|
26
|
+
const operations = {
|
|
27
|
+
Queries: new Set<string>(),
|
|
28
|
+
Mutations: new Set<string>(),
|
|
29
|
+
Relations: new Set<string>(),
|
|
30
|
+
Subscriptions: new Set<string>(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
//Enumerates all relations/subqueries
|
|
34
|
+
builder.hook(
|
|
35
|
+
'GraphQLObjectType:fields:field',
|
|
36
|
+
(field, _build, { scope }) => {
|
|
37
|
+
if (
|
|
38
|
+
scope.fieldName !== 'query' &&
|
|
39
|
+
(scope.isPgForwardRelationField || scope.isPgBackwardRelationField)
|
|
40
|
+
) {
|
|
41
|
+
operations.Relations.add(scope.fieldName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return field;
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
// Enumerate all queries, mutations and subscriptions
|
|
48
|
+
builder.hook('GraphQLObjectType:fields', (fields, _build, context) => {
|
|
49
|
+
if (context.Self.name === 'Query') {
|
|
50
|
+
Object.keys(fields).forEach((q) => {
|
|
51
|
+
if (q !== 'query') {
|
|
52
|
+
operations.Queries.add(q);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (context.Self.name === 'Mutation') {
|
|
58
|
+
Object.keys(fields).forEach((m) => operations.Mutations.add(m));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (context.Self.name === 'Subscription') {
|
|
62
|
+
Object.keys(fields).forEach((s) => operations.Subscriptions.add(s));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return fields;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Write enums to file when the schema is built.
|
|
69
|
+
builder.hook('finalize', (schema, _build, _context) => {
|
|
70
|
+
const operationsToEnum: string[] = [];
|
|
71
|
+
for (const [key, value] of Object.entries(operations)) {
|
|
72
|
+
const outputEnumPath = path.join(
|
|
73
|
+
options.outRoot,
|
|
74
|
+
`${key.toLowerCase()}.ts`,
|
|
75
|
+
);
|
|
76
|
+
// Exit early if expected operations are not there and assume there was an error during GQL schema generation.
|
|
77
|
+
// Do not write any files, keep whatever was already on the disk.
|
|
78
|
+
if (value.size === 0) {
|
|
79
|
+
if (fs.existsSync(outputEnumPath)) {
|
|
80
|
+
operationsToEnum.push(key);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
writeSourceFile(outputEnumPath, generateEnum(key, [...value]));
|
|
86
|
+
operationsToEnum.push(key);
|
|
87
|
+
}
|
|
88
|
+
writeSourceFile(
|
|
89
|
+
path.join(options.outRoot, 'index.ts'),
|
|
90
|
+
generateIndex(operationsToEnum.map((o) => o.toLowerCase())),
|
|
91
|
+
);
|
|
92
|
+
return schema;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Writes generated source code to file but compares with the content of the existing file first to avoid triggering compilation in watch mode.
|
|
99
|
+
* @param outPath - Path where to write the source file.
|
|
100
|
+
* @param contents - Contents of the source file.
|
|
101
|
+
*/
|
|
102
|
+
function writeSourceFile(outPath: string, contents: string): void {
|
|
103
|
+
if (!fs.existsSync(path.dirname(outPath))) {
|
|
104
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
// Don't write file if the contents are the same, avoid triggering a build in watch mode.
|
|
107
|
+
if (fs.existsSync(outPath)) {
|
|
108
|
+
const currentContents = fs.readFileSync(outPath, { encoding: 'utf-8' });
|
|
109
|
+
if (currentContents.toString() === contents) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
fs.writeFileSync(outPath, contents, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function generateEnum(name: string, members: string[]): string {
|
|
117
|
+
return endent`export enum ${name} {
|
|
118
|
+
${members
|
|
119
|
+
.sort()
|
|
120
|
+
.map((m) => `${m} = '${m}'`)
|
|
121
|
+
.join(',\n')}
|
|
122
|
+
}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function generateIndex(modules: string[]): string {
|
|
126
|
+
return modules
|
|
127
|
+
.sort()
|
|
128
|
+
.map((m) => `export * from './${m}';`)
|
|
129
|
+
.join('\n');
|
|
130
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { QueryBuilder, SQL } from 'graphile-build-pg';
|
|
3
|
+
import {
|
|
4
|
+
ExtensionDefinition,
|
|
5
|
+
gql,
|
|
6
|
+
makeExtendSchemaPlugin,
|
|
7
|
+
} from 'graphile-utils';
|
|
8
|
+
// graphile-utils doesn't export this yet
|
|
9
|
+
import { GraphQLResolveInfo } from 'graphql';
|
|
10
|
+
import inflection from 'inflection';
|
|
11
|
+
import { Build, Plugin } from 'postgraphile';
|
|
12
|
+
|
|
13
|
+
type GraphileHelpers = any;
|
|
14
|
+
type AugmentedGraphQLFieldResolver<
|
|
15
|
+
TSource,
|
|
16
|
+
TContext,
|
|
17
|
+
TArgs = { [argName: string]: any },
|
|
18
|
+
> = (
|
|
19
|
+
parent: TSource,
|
|
20
|
+
args: TArgs,
|
|
21
|
+
context: TContext,
|
|
22
|
+
info: GraphQLResolveInfo & {
|
|
23
|
+
graphile: GraphileHelpers;
|
|
24
|
+
},
|
|
25
|
+
) => any;
|
|
26
|
+
/* The JSON object that `tg__graphql_subscription()` delivers via NOTIFY */
|
|
27
|
+
interface ITgGraphQLSubscriptionPayload {
|
|
28
|
+
event: string;
|
|
29
|
+
subject: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* This function handles the boilerplate of fetching a record from the database
|
|
34
|
+
* which has the 'id' equal to the 'subject' from the PG NOTIFY event payload
|
|
35
|
+
* (see `tg__graphql_subscription()` trigger function in the database).
|
|
36
|
+
*/
|
|
37
|
+
function recordByIdFromTable(
|
|
38
|
+
build: Build,
|
|
39
|
+
sqlTable: SQL,
|
|
40
|
+
): AugmentedGraphQLFieldResolver<ITgGraphQLSubscriptionPayload, any> {
|
|
41
|
+
const { pgSql: sql } = build;
|
|
42
|
+
return async (
|
|
43
|
+
event: ITgGraphQLSubscriptionPayload,
|
|
44
|
+
_args: Record<string, unknown>,
|
|
45
|
+
_context: any,
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/typedef
|
|
47
|
+
{ graphile: { selectGraphQLResultFromTable } },
|
|
48
|
+
) => {
|
|
49
|
+
const rows = await selectGraphQLResultFromTable(
|
|
50
|
+
sqlTable,
|
|
51
|
+
(tableAlias: SQL, sqlBuilder: QueryBuilder) => {
|
|
52
|
+
sqlBuilder.where(
|
|
53
|
+
sql.fragment`${tableAlias}.id = ${sql.value(event.subject)}`,
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
return rows[0];
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getId(): AugmentedGraphQLFieldResolver<
|
|
62
|
+
ITgGraphQLSubscriptionPayload,
|
|
63
|
+
any
|
|
64
|
+
> {
|
|
65
|
+
return async (event: ITgGraphQLSubscriptionPayload) => event.subject;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Factory function to create a makeExtendSchemaPlugin that adds a subscription endpoint for specific entity.
|
|
70
|
+
* @param mainTableName name of the table in the database without schema
|
|
71
|
+
* @param typeName name of the GraphQL type that is generated by postgraphile for `mainTableName` table
|
|
72
|
+
* @param gqlIdDataType optional parameter to set an ID type for the endpoint. Supported types are `Int` and `UUID`. Default is `Int`.
|
|
73
|
+
*/
|
|
74
|
+
export const SubscriptionsPluginFactory = (
|
|
75
|
+
mainTableName: string,
|
|
76
|
+
typeName: string,
|
|
77
|
+
gqlIdDataType: 'Int' | 'UUID' = 'Int',
|
|
78
|
+
): Plugin =>
|
|
79
|
+
makeExtendSchemaPlugin((build: Build) => {
|
|
80
|
+
const { pgSql: sql } = build;
|
|
81
|
+
const variableName = inflection.camelize(typeName, true);
|
|
82
|
+
const payloadName =
|
|
83
|
+
inflection.camelize(typeName, false) + 'SubscriptionPayload';
|
|
84
|
+
const topicName = 'graphql:' + mainTableName;
|
|
85
|
+
return {
|
|
86
|
+
typeDefs: gql`
|
|
87
|
+
type ${payloadName} {
|
|
88
|
+
id: ${gqlIdDataType}! # Populated by our resolver below
|
|
89
|
+
${variableName}: ${typeName} # Populated by our resolver below
|
|
90
|
+
event: String # Part of the NOTIFY payload
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
extend type Subscription {
|
|
94
|
+
"""
|
|
95
|
+
Triggered when a ${typeName} is mutated (insert, update or delete).
|
|
96
|
+
"""
|
|
97
|
+
${variableName}Mutated: ${payloadName}
|
|
98
|
+
@pgSubscription(topic: "${topicName}")
|
|
99
|
+
}
|
|
100
|
+
`,
|
|
101
|
+
resolvers: {
|
|
102
|
+
[payloadName]: {
|
|
103
|
+
id: getId(),
|
|
104
|
+
[variableName]: recordByIdFromTable(
|
|
105
|
+
build,
|
|
106
|
+
sql.fragment([`app_public.${mainTableName}`]),
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
Subscription: {
|
|
110
|
+
[`${variableName}Mutated`]: (event) => event,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
} as ExtensionDefinition;
|
|
114
|
+
});
|