@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.
Files changed (95) hide show
  1. package/README.md +16 -0
  2. package/dist/common/checks.d.ts +9 -0
  3. package/dist/common/checks.d.ts.map +1 -0
  4. package/dist/common/checks.js +21 -0
  5. package/dist/common/checks.js.map +1 -0
  6. package/dist/common/index.d.ts +3 -0
  7. package/dist/common/index.d.ts.map +1 -0
  8. package/dist/common/index.js +19 -0
  9. package/dist/common/index.js.map +1 -0
  10. package/dist/common/types.d.ts +13 -0
  11. package/dist/common/types.d.ts.map +1 -0
  12. package/dist/common/types.js +15 -0
  13. package/dist/common/types.js.map +1 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +20 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/plugins/add-error-codes-enum-plugin.d.ts +27 -0
  19. package/dist/plugins/add-error-codes-enum-plugin.d.ts.map +1 -0
  20. package/dist/plugins/add-error-codes-enum-plugin.js +78 -0
  21. package/dist/plugins/add-error-codes-enum-plugin.js.map +1 -0
  22. package/dist/plugins/annotate-types-with-permissions-plugin.d.ts +22 -0
  23. package/dist/plugins/annotate-types-with-permissions-plugin.d.ts.map +1 -0
  24. package/dist/plugins/annotate-types-with-permissions-plugin.js +145 -0
  25. package/dist/plugins/annotate-types-with-permissions-plugin.js.map +1 -0
  26. package/dist/plugins/deprecate-stray-node-id-fields-plugin.d.ts +14 -0
  27. package/dist/plugins/deprecate-stray-node-id-fields-plugin.d.ts.map +1 -0
  28. package/dist/plugins/deprecate-stray-node-id-fields-plugin.js +37 -0
  29. package/dist/plugins/deprecate-stray-node-id-fields-plugin.js.map +1 -0
  30. package/dist/plugins/generic-bulk-plugin-factory.d.ts +49 -0
  31. package/dist/plugins/generic-bulk-plugin-factory.d.ts.map +1 -0
  32. package/dist/plugins/generic-bulk-plugin-factory.js +181 -0
  33. package/dist/plugins/generic-bulk-plugin-factory.js.map +1 -0
  34. package/dist/plugins/graphiql-management-mode-plugin-hook.d.ts +13 -0
  35. package/dist/plugins/graphiql-management-mode-plugin-hook.d.ts.map +1 -0
  36. package/dist/plugins/graphiql-management-mode-plugin-hook.js +44 -0
  37. package/dist/plugins/graphiql-management-mode-plugin-hook.js.map +1 -0
  38. package/dist/plugins/index.d.ts +10 -0
  39. package/dist/plugins/index.d.ts.map +1 -0
  40. package/dist/plugins/index.js +26 -0
  41. package/dist/plugins/index.js.map +1 -0
  42. package/dist/plugins/omit-from-query-root-plugin.d.ts +17 -0
  43. package/dist/plugins/omit-from-query-root-plugin.d.ts.map +1 -0
  44. package/dist/plugins/omit-from-query-root-plugin.js +43 -0
  45. package/dist/plugins/omit-from-query-root-plugin.js.map +1 -0
  46. package/dist/plugins/operations-enum-generator-plugin-factory.d.ts +15 -0
  47. package/dist/plugins/operations-enum-generator-plugin-factory.d.ts.map +1 -0
  48. package/dist/plugins/operations-enum-generator-plugin-factory.js +108 -0
  49. package/dist/plugins/operations-enum-generator-plugin-factory.js.map +1 -0
  50. package/dist/plugins/subscriptions-plugin-factory.d.ts +9 -0
  51. package/dist/plugins/subscriptions-plugin-factory.d.ts.map +1 -0
  52. package/dist/plugins/subscriptions-plugin-factory.js +67 -0
  53. package/dist/plugins/subscriptions-plugin-factory.js.map +1 -0
  54. package/dist/plugins/validation-directives-plugin.d.ts +6 -0
  55. package/dist/plugins/validation-directives-plugin.d.ts.map +1 -0
  56. package/dist/plugins/validation-directives-plugin.js +117 -0
  57. package/dist/plugins/validation-directives-plugin.js.map +1 -0
  58. package/dist/postgraphile/enhance-graphql-errors.d.ts +48 -0
  59. package/dist/postgraphile/enhance-graphql-errors.d.ts.map +1 -0
  60. package/dist/postgraphile/enhance-graphql-errors.js +67 -0
  61. package/dist/postgraphile/enhance-graphql-errors.js.map +1 -0
  62. package/dist/postgraphile/index.d.ts +4 -0
  63. package/dist/postgraphile/index.d.ts.map +1 -0
  64. package/dist/postgraphile/index.js +20 -0
  65. package/dist/postgraphile/index.js.map +1 -0
  66. package/dist/postgraphile/postgraphile-options-builder.d.ts +273 -0
  67. package/dist/postgraphile/postgraphile-options-builder.d.ts.map +1 -0
  68. package/dist/postgraphile/postgraphile-options-builder.js +419 -0
  69. package/dist/postgraphile/postgraphile-options-builder.js.map +1 -0
  70. package/dist/postgraphile/websocket-utils.d.ts +11 -0
  71. package/dist/postgraphile/websocket-utils.d.ts.map +1 -0
  72. package/dist/postgraphile/websocket-utils.js +17 -0
  73. package/dist/postgraphile/websocket-utils.js.map +1 -0
  74. package/package.json +61 -0
  75. package/src/common/checks.ts +23 -0
  76. package/src/common/index.ts +2 -0
  77. package/src/common/types.ts +15 -0
  78. package/src/index.ts +3 -0
  79. package/src/plugins/add-error-codes-enum-plugin.ts +102 -0
  80. package/src/plugins/annotate-types-with-permissions-plugin.spec.ts +158 -0
  81. package/src/plugins/annotate-types-with-permissions-plugin.ts +205 -0
  82. package/src/plugins/deprecate-stray-node-id-fields-plugin.ts +41 -0
  83. package/src/plugins/generic-bulk-plugin-factory.ts +313 -0
  84. package/src/plugins/graphiql-management-mode-plugin-hook.ts +46 -0
  85. package/src/plugins/index.ts +9 -0
  86. package/src/plugins/omit-from-query-root-plugin.ts +69 -0
  87. package/src/plugins/operations-enum-generator-plugin-factory.ts +130 -0
  88. package/src/plugins/subscriptions-plugin-factory.ts +114 -0
  89. package/src/plugins/validation-directives-plugin.ts +141 -0
  90. package/src/postgraphile/enhance-graphql-errors.spec.ts +241 -0
  91. package/src/postgraphile/enhance-graphql-errors.ts +138 -0
  92. package/src/postgraphile/index.ts +3 -0
  93. package/src/postgraphile/postgraphile-options-builder.spec.ts +744 -0
  94. package/src/postgraphile/postgraphile-options-builder.ts +510 -0
  95. 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
+ });