@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,158 @@
1
+ import 'jest-extended';
2
+ import { exportedForTestingAnnotatePlugin } from './annotate-types-with-permissions-plugin';
3
+ const getPolicy = (
4
+ overrides: {
5
+ permissive?: string;
6
+ command?: string;
7
+ using?: string;
8
+ withcheck?: string;
9
+ } = {},
10
+ ): any => {
11
+ return {
12
+ schemaname: 'app_public',
13
+ tablename: 'entities',
14
+ permissive: 'PERMISSIVE',
15
+ command: 'SELECT',
16
+ using: `((SELECT user_has_permission('READER,EDITOR,ADMIN'::text)) AND ((entity_type)::text = 'MOVIE_GENRE'::text))`,
17
+ withcheck: null,
18
+ ...overrides,
19
+ };
20
+ };
21
+
22
+ describe('AnnotateTypesWithPermissionsPlugin', () => {
23
+ describe('setPermissionsCommentText', () => {
24
+ it.each([undefined, null, '', ' '])(
25
+ 'empty original description without policies -> empty result',
26
+ (description) => {
27
+ // Act
28
+ const result =
29
+ exportedForTestingAnnotatePlugin.setPermissionsCommentText(
30
+ description,
31
+ [],
32
+ 'INSERT',
33
+ );
34
+
35
+ // Assert
36
+ expect(result).toBe('');
37
+ },
38
+ );
39
+
40
+ it.each([undefined, null, '', ' '])(
41
+ 'empty original description with matching policy -> permissions without description',
42
+ (description) => {
43
+ // Act
44
+ const result =
45
+ exportedForTestingAnnotatePlugin.setPermissionsCommentText(
46
+ description,
47
+ [getPolicy()],
48
+ 'SELECT',
49
+ );
50
+
51
+ // Assert
52
+ expect(result).toBe('@permissions: READER,EDITOR,ADMIN');
53
+ },
54
+ );
55
+
56
+ it('non-empty original description without matching policy -> description without permissions', () => {
57
+ // Act
58
+ const result = exportedForTestingAnnotatePlugin.setPermissionsCommentText(
59
+ 'Test description of some GraphQL type.',
60
+ [getPolicy({ command: 'UPDATE' })],
61
+ 'SELECT',
62
+ );
63
+
64
+ // Assert
65
+ expect(result).toBe('Test description of some GraphQL type.');
66
+ });
67
+
68
+ it.each(['SELECT', 'ALL'])(
69
+ 'non-empty original description with matching policy -> permissions with description',
70
+ (command) => {
71
+ // Act
72
+ const result =
73
+ exportedForTestingAnnotatePlugin.setPermissionsCommentText(
74
+ 'Test description of some GraphQL type.',
75
+ [getPolicy({ command })],
76
+ 'SELECT',
77
+ );
78
+
79
+ // Assert
80
+ expect(result).toBe(
81
+ 'Test description of some GraphQL type.\n@permissions: READER,EDITOR,ADMIN',
82
+ );
83
+ },
84
+ );
85
+
86
+ it('non-empty original description with restrictive and permissive policies -> description with restrictive permissions', () => {
87
+ // Act
88
+ const result = exportedForTestingAnnotatePlugin.setPermissionsCommentText(
89
+ 'Test description of some GraphQL type.',
90
+ [
91
+ getPolicy({
92
+ command: 'DELETE',
93
+ permissive: 'RESTRICTIVE',
94
+ using: `user_has_permission('ADMIN'::text)`,
95
+ }),
96
+ getPolicy({
97
+ command: 'ALL',
98
+ permissive: 'PERMISSIVE',
99
+ using: `(user_has_permission('READER,EDITOR,ADMIN'::text) AND (1 = 1))`,
100
+ withcheck: `(user_has_permission('EDITOR,ADMIN'::text) AND (1 = 1))`,
101
+ }),
102
+ ],
103
+ 'DELETE',
104
+ );
105
+
106
+ // Assert
107
+ expect(result).toBe(
108
+ 'Test description of some GraphQL type.\n@permissions: ADMIN',
109
+ );
110
+ });
111
+
112
+ it('non-empty original description with multiple matching policies -> description with multiple permission sets', () => {
113
+ // Act
114
+ const result = exportedForTestingAnnotatePlugin.setPermissionsCommentText(
115
+ 'Test description of some GraphQL type.',
116
+ [
117
+ // Backwards-compatibility control case
118
+ getPolicy({
119
+ command: 'UPDATE',
120
+ permissive: 'PERMISSIVE',
121
+ using: `(user_has_permission('TVSHOWS_EDIT,ADMIN'::text) AND ((entity_type)::text = 'TVSHOW'::text))`,
122
+ withcheck: `(user_has_permission('TVSHOWS_EDIT,ADMIN'::text) AND ((entity_type)::text = 'TVSHOW'::text))`,
123
+ }),
124
+ getPolicy({
125
+ command: 'UPDATE',
126
+ permissive: 'PERMISSIVE',
127
+ using: `((SELECT user_has_permission('SETTINGS_EDIT,ADMIN'::text)) AND ((entity_type)::text = 'MOVIE_GENRE'::text))`,
128
+ withcheck: `((SELECT user_has_permission('SETTINGS_EDIT,ADMIN'::text)) AND ((entity_type)::text = 'MOVIE_GENRE'::text))`,
129
+ }),
130
+ getPolicy({
131
+ command: 'UPDATE',
132
+ permissive: 'PERMISSIVE',
133
+ using: `((SELECT user_has_permission('MOVIES_EDIT,ADMIN'::text)) AND ((entity_type)::text = 'MOVIE'::text))`,
134
+ withcheck: `((SELECT user_has_permission('MOVIES_EDIT,ADMIN'::text)) AND ((entity_type)::text = 'MOVIE'::text))`,
135
+ }),
136
+ getPolicy({
137
+ command: 'UPDATE',
138
+ permissive: 'PERMISSIVE',
139
+ using: `((SELECT user_has_permission('TVSHOWS_EDIT,ADMIN'::text)) AND ((entity_type)::text = 'EPISODE'::text))`,
140
+ withcheck: `((SELECT user_has_permission('TVSHOWS_EDIT,ADMIN'::text)) AND ((entity_type)::text = 'EPISODE'::text))`,
141
+ }),
142
+ getPolicy({
143
+ command: 'UPDATE',
144
+ permissive: 'PERMISSIVE',
145
+ using: `((SELECT user_has_permission('SETTINGS_EDIT,ADMIN'::text)) AND ((entity_type)::text = 'TVSHOW_GENRE'::text))`,
146
+ withcheck: `((SELECT user_has_permission('SETTINGS_EDIT,ADMIN'::text)) AND ((entity_type)::text = 'TVSHOW_GENRE'::text))`,
147
+ }),
148
+ ],
149
+ 'UPDATE',
150
+ );
151
+
152
+ // Assert
153
+ expect(result).toBe(
154
+ 'Test description of some GraphQL type.\n@permissions: MOVIES_EDIT,ADMIN\n@permissions: SETTINGS_EDIT,ADMIN\n@permissions: TVSHOWS_EDIT,ADMIN',
155
+ );
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,205 @@
1
+ import { Pool } from 'pg';
2
+ import { Plugin } from 'postgraphile';
3
+ import { isNullOrWhitespace } from '../common';
4
+
5
+ interface PolicyRow {
6
+ schemaname: string;
7
+ tablename: string;
8
+ permissive: 'PERMISSIVE' | 'RESTRICTIVE';
9
+ command: string;
10
+ using: string | null;
11
+ withcheck: string | null;
12
+ }
13
+
14
+ type SqlOperation = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE';
15
+ type AllOperations = 'ALL' | SqlOperation;
16
+
17
+ async function loadPolicies(ownerPool: Pool): Promise<PolicyRow[]> {
18
+ const client = await ownerPool.connect();
19
+ try {
20
+ // get all policies, sort so "ALL" is last and custom ones come earlier
21
+ const queryResult = await client.query(`
22
+ select
23
+ schemaname,
24
+ tablename,
25
+ permissive,
26
+ cmd as command,
27
+ qual as using,
28
+ with_check as withcheck
29
+ from pg_policies
30
+ order by command desc`);
31
+ return queryResult.rows;
32
+ } finally {
33
+ client.release();
34
+ }
35
+ }
36
+
37
+ function parsePermissions(sqlPolicy: string): string {
38
+ const matches = /(ax_utils\.)?user_has_permission\('([^']+)/.exec(sqlPolicy);
39
+ return matches && matches.length > 2 ? matches[2] : '';
40
+ }
41
+
42
+ function getPermissions(
43
+ policies: PolicyRow[],
44
+ commands: AllOperations[],
45
+ field: 'using' | 'withcheck',
46
+ ): string[] {
47
+ let matchingPolicies = policies.filter(
48
+ (p) => commands.find((cmd) => p.command === cmd) && p[field],
49
+ );
50
+
51
+ // If RESTRICTIVE policies found, list only them and remove PERMISSIVE ones
52
+ if (matchingPolicies.some((policy) => policy.permissive === 'RESTRICTIVE')) {
53
+ matchingPolicies = matchingPolicies.filter(
54
+ (policy) => policy.permissive === 'RESTRICTIVE',
55
+ );
56
+ }
57
+
58
+ return [
59
+ ...new Set<string>( // removes duplicates
60
+ matchingPolicies
61
+ .map((p) => parsePermissions(p[field] as string))
62
+ .filter((p) => !isNullOrWhitespace(p))
63
+ .sort(),
64
+ ),
65
+ ];
66
+ }
67
+
68
+ function setPermissionsCommentText(
69
+ currentDescription: string | null | undefined,
70
+ policies: PolicyRow[],
71
+ type: SqlOperation,
72
+ ): string {
73
+ const field = type === 'SELECT' || type === 'DELETE' ? 'using' : 'withcheck';
74
+
75
+ const permissions = getPermissions(policies, ['ALL', type], field);
76
+ const description = isNullOrWhitespace(currentDescription)
77
+ ? ''
78
+ : currentDescription;
79
+ return permissions.length > 0
80
+ ? [
81
+ isNullOrWhitespace(description) ? description : `${description}\n`,
82
+ ...permissions
83
+ .map((permission) => `@permissions: ${permission}`)
84
+ .join('\n'),
85
+ ].join('')
86
+ : description;
87
+ }
88
+
89
+ // all available hooks are defined here:
90
+ // https://www.graphile.org/graphile-build/all-hooks/
91
+ /**
92
+ * Plugin that adds permissions annotation to graphql schema descriptions.
93
+ *
94
+ * @param graphileBuildOptions should be of type `Partial<Options> & { ownerPool: Pool }`
95
+ */
96
+ export const AnnotateTypesWithPermissionsPlugin: Plugin = async (
97
+ builder,
98
+ { ownerPool },
99
+ ) => {
100
+ const allPolicies = await loadPolicies(ownerPool);
101
+ builder.hook('GraphQLInputObjectType', (obj, _build, context) => {
102
+ const scope = context.scope;
103
+
104
+ // PostGraphile input Types that do not correspond to a database table
105
+ if (
106
+ scope.isPointInputType ||
107
+ scope.isIntervalInputType ||
108
+ scope.isPgConnectionFilterOperators ||
109
+ scope.isPgConnectionFilterMany ||
110
+ scope.isPgCondition ||
111
+ scope.isPgConnectionFilter
112
+ ) {
113
+ return obj;
114
+ }
115
+
116
+ // PostGraphile generated input types for DB mutations
117
+ if (scope.pgIntrospection) {
118
+ const policies = allPolicies.filter(
119
+ (value) =>
120
+ value.schemaname === scope.pgIntrospection.namespaceName &&
121
+ value.tablename === scope.pgIntrospection.name,
122
+ );
123
+
124
+ let operation: SqlOperation;
125
+
126
+ switch (true) {
127
+ case scope.isPgCreateInputType:
128
+ operation = 'INSERT';
129
+ break;
130
+ case scope.isPgUpdateInputType:
131
+ operation = 'UPDATE';
132
+ break;
133
+ case scope.isPgDeleteInputType:
134
+ operation = 'DELETE';
135
+ break;
136
+ default:
137
+ return obj;
138
+ }
139
+
140
+ obj.description = setPermissionsCommentText(
141
+ obj.description,
142
+ policies,
143
+ operation,
144
+ );
145
+ return obj;
146
+ }
147
+
148
+ // The input objects that come to this code-path are custom input types
149
+ // and require custom logic to protect them!
150
+ // Only works if they have input >types< - if they have no input or scalar
151
+ // input values they cannot be found by this method.
152
+ // Example: PopulateInput from the populate endpoint
153
+ return obj;
154
+ });
155
+
156
+ builder.hook('GraphQLObjectType', (obj, _build, context) => {
157
+ const scope = context.scope;
158
+
159
+ if (
160
+ // PostGraphile Types that do not correspond to a database table
161
+ scope.isPointType ||
162
+ scope.isIntervalType ||
163
+ scope.isPgConnectionFilterOperators ||
164
+ scope.isPgConnectionFilterMany ||
165
+ scope.isPgCondition ||
166
+ scope.isPgConnectionFilter ||
167
+ scope.isPageInfo ||
168
+ scope.isEdgeType ||
169
+ scope.isRootQuery ||
170
+ scope.isRootMutation ||
171
+ scope.isRootSubscription ||
172
+ scope.isMutationPayload ||
173
+ scope.isPgDeletePayloadType ||
174
+ scope.isPgUpdatePayloadType ||
175
+ // TODO: Procedures have no row level security: secure them in another way
176
+ context?.scope?.pgIntrospection?.kind === 'procedure'
177
+ ) {
178
+ return obj;
179
+ }
180
+
181
+ // PostGraphile generated types for DB queries
182
+ if (scope.pgIntrospection) {
183
+ const policies = allPolicies.filter(
184
+ (value) =>
185
+ value.schemaname === scope.pgIntrospection.namespaceName &&
186
+ value.tablename === scope.pgIntrospection.name,
187
+ );
188
+
189
+ obj.description = setPermissionsCommentText(
190
+ obj.description,
191
+ policies,
192
+ 'SELECT',
193
+ );
194
+ return obj;
195
+ }
196
+
197
+ // The objects that come to this code-path are custom types
198
+ // and require custom logic to protect them!
199
+ // Only works if they have >types< - if they have no input or scalar
200
+ // input values they cannot be found by this method.
201
+ return obj;
202
+ });
203
+ };
204
+
205
+ export const exportedForTestingAnnotatePlugin = { setPermissionsCommentText };
@@ -0,0 +1,41 @@
1
+ import { Plugin } from 'postgraphile';
2
+ import { UnreachableCaseError } from '../common';
3
+
4
+ type DeprecateMode = 'DEPRECATE' | 'DROP';
5
+
6
+ /**
7
+ * NodePlugin is excluded by default to remove all references of NodeId fields
8
+ * and byNodeId operations. A few such instances are not removed and handled
9
+ * explicitly by this plugin, e.g. deletedEntityNodeId in the
10
+ * DeleteEntityPayload types.
11
+ *
12
+ * By default, the plugin marks each field as deprecated. Pass an explicit
13
+ * `DROP` value to drop the fields instead.
14
+ */
15
+ export const DeprecateStrayNodeIdFieldsPlugin =
16
+ (mode: DeprecateMode = 'DEPRECATE'): Plugin =>
17
+ (builder) => {
18
+ builder.hook('GraphQLObjectType:fields', (fields, _build, context) => {
19
+ const typeName = context.Self.name as string;
20
+ if (typeName.startsWith('Delete') && typeName.endsWith('Payload')) {
21
+ Object.keys(fields)
22
+ .filter(
23
+ (name) => name.startsWith('deleted') && name.endsWith('NodeId'),
24
+ )
25
+ .map((name) => {
26
+ switch (mode) {
27
+ case 'DEPRECATE':
28
+ fields[name].deprecationReason = 'The field is obsolete.';
29
+ break;
30
+ case 'DROP':
31
+ delete fields[name];
32
+ break;
33
+
34
+ default:
35
+ throw new UnreachableCaseError(mode);
36
+ }
37
+ });
38
+ }
39
+ return fields;
40
+ });
41
+ };