@nextage/nx-frame-be 1.0.47 → 1.0.48

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.
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /* -------------------------------------------------------------------------- */
3
+ /* Per-operation @auth role overrides */
4
+ /* */
5
+ /* These tests assert the SDL emitted by the generator, not a running server: */
6
+ /* the point is WHICH directive is attached to WHICH generated operation. */
7
+ /* Mounting Apollo here is impossible anyway, since registering the same */
8
+ /* controller twice collides on `type Mutation`. */
9
+ /* -------------------------------------------------------------------------- */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const coded_entity_controller_1 = require("../../models/coded-entity.controller");
12
+ const generator_1 = require("../generator");
13
+ const loader_1 = require("../loader");
14
+ const READ = 'READ_ROLE';
15
+ const WRITE = 'WRITE_ROLE';
16
+ const NARROW = 'CROSS_TENANT_ROLE';
17
+ /**
18
+ * `loadSchema` APPENDS to the process-wide `mainSchema`, so every scenario has to
19
+ * start from an empty accumulator or it would read the previous scenario's SDL.
20
+ */
21
+ function generate(roles) {
22
+ loader_1.mainSchema.typeDefs.query = '';
23
+ loader_1.mainSchema.typeDefs.mutation = '';
24
+ loader_1.mainSchema.typeDefs.subscription = '';
25
+ loader_1.mainSchema.typeDefs.type = '';
26
+ loader_1.mainSchema.resolvers = {};
27
+ (0, generator_1.generateSchema)(coded_entity_controller_1.ceCtrl, roles ? { roles } : {});
28
+ return {
29
+ query: loader_1.mainSchema.typeDefs.query,
30
+ mutation: loader_1.mainSchema.typeDefs.mutation,
31
+ subscription: loader_1.mainSchema.typeDefs.subscription
32
+ };
33
+ }
34
+ /**
35
+ * Return the `@auth` directive argument attached to one generated operation, or
36
+ * `null` when the operation is absent from the SDL.
37
+ *
38
+ * IMPORTANT: the match is anchored on the FIELD (`name(`), never on the bare name:
39
+ * `codedEntity` is a prefix of `codedEntityList`, and asserting on the bare name
40
+ * would silently read the wrong line.
41
+ */
42
+ function authOf(sdl, operation) {
43
+ var _a;
44
+ const line = sdl.split('\n').find(l => l.trim().startsWith(`${operation}(`));
45
+ if (!line)
46
+ return null;
47
+ const match = line.match(/@auth(\(requires: "[^"]*"\))?/);
48
+ return match ? ((_a = match[1]) !== null && _a !== void 0 ? _a : '') : null;
49
+ }
50
+ const requires = (roles) => `(requires: "${roles}")`;
51
+ /* -------------------------------------------------------------------------- */
52
+ /* Default behaviour (non-regression) */
53
+ /* -------------------------------------------------------------------------- */
54
+ it('keeps read on queries and write on mutations when no override is given', () => {
55
+ const sdl = generate({ read: READ, write: WRITE });
56
+ expect(authOf(sdl.query, 'codedEntityCount')).toEqual(requires(READ));
57
+ expect(authOf(sdl.query, 'codedEntityList')).toEqual(requires(READ));
58
+ expect(authOf(sdl.query, 'codedEntity')).toEqual(requires(READ));
59
+ expect(authOf(sdl.query, 'findOneCodedEntity')).toEqual(requires(READ));
60
+ expect(authOf(sdl.mutation, 'createCodedEntity')).toEqual(requires(WRITE));
61
+ expect(authOf(sdl.mutation, 'updateCodedEntity')).toEqual(requires(WRITE));
62
+ expect(authOf(sdl.mutation, 'removeCodedEntity')).toEqual(requires(WRITE));
63
+ expect(authOf(sdl.mutation, 'copyCodedEntity')).toEqual(requires(WRITE));
64
+ expect(authOf(sdl.mutation, 'bulkRemoveCodedEntity')).toEqual(requires(WRITE));
65
+ });
66
+ it('emits a bare @auth when a role list is not configured', () => {
67
+ const sdl = generate({ write: WRITE });
68
+ expect(authOf(sdl.query, 'codedEntityList')).toEqual('');
69
+ expect(authOf(sdl.mutation, 'removeCodedEntity')).toEqual(requires(WRITE));
70
+ });
71
+ it('still accepts a plain role list as read and write', () => {
72
+ const sdl = generate([READ, WRITE]);
73
+ expect(authOf(sdl.query, 'codedEntityList')).toEqual(requires(`${READ},${WRITE}`));
74
+ expect(authOf(sdl.mutation, 'removeCodedEntity')).toEqual(requires(`${READ},${WRITE}`));
75
+ });
76
+ /* -------------------------------------------------------------------------- */
77
+ /* Overrides */
78
+ /* -------------------------------------------------------------------------- */
79
+ it('narrows a single mutation and leaves its siblings on write', () => {
80
+ const sdl = generate({ read: READ, write: WRITE, overrides: { remove: NARROW } });
81
+ expect(authOf(sdl.mutation, 'removeCodedEntity')).toEqual(requires(NARROW));
82
+ expect(authOf(sdl.mutation, 'createCodedEntity')).toEqual(requires(WRITE));
83
+ expect(authOf(sdl.mutation, 'updateCodedEntity')).toEqual(requires(WRITE));
84
+ expect(authOf(sdl.mutation, 'copyCodedEntity')).toEqual(requires(WRITE));
85
+ expect(authOf(sdl.mutation, 'bulkRemoveCodedEntity')).toEqual(requires(WRITE));
86
+ expect(authOf(sdl.mutation, 'removeCodedEntityArrayItem')).toEqual(requires(WRITE));
87
+ });
88
+ it('narrows a single query and leaves its siblings on read', () => {
89
+ const sdl = generate({ read: READ, write: WRITE, overrides: { list: NARROW } });
90
+ expect(authOf(sdl.query, 'codedEntityList')).toEqual(requires(NARROW));
91
+ expect(authOf(sdl.query, 'codedEntityCount')).toEqual(requires(READ));
92
+ expect(authOf(sdl.query, 'codedEntity')).toEqual(requires(READ));
93
+ expect(authOf(sdl.query, 'findOneCodedEntity')).toEqual(requires(READ));
94
+ });
95
+ it('joins a multi-role override into a single requires argument', () => {
96
+ const sdl = generate({ read: READ, write: WRITE, overrides: { remove: [NARROW, 'ADMIN'] } });
97
+ expect(authOf(sdl.mutation, 'removeCodedEntity')).toEqual(requires(`${NARROW},ADMIN`));
98
+ });
99
+ /* -------------------------------------------------------------------------- */
100
+ /* Fail-safe */
101
+ /* -------------------------------------------------------------------------- */
102
+ it('falls back to the default instead of publishing the operation when an override is empty', () => {
103
+ const empty = generate({ read: READ, write: WRITE, overrides: { remove: [], list: '' } });
104
+ expect(authOf(empty.mutation, 'removeCodedEntity')).toEqual(requires(WRITE));
105
+ expect(authOf(empty.query, 'codedEntityList')).toEqual(requires(READ));
106
+ const missing = generate({ read: READ, write: WRITE, overrides: { remove: undefined } });
107
+ expect(authOf(missing.mutation, 'removeCodedEntity')).toEqual(requires(WRITE));
108
+ });
109
+ /* -------------------------------------------------------------------------- */
110
+ /* Subscriptions */
111
+ /* -------------------------------------------------------------------------- */
112
+ it('does not let an override reach the subscriptions, which always follow read', () => {
113
+ const sdl = generate({ read: READ, write: WRITE, overrides: { remove: NARROW } });
114
+ expect(sdl.subscription.match(new RegExp(`@auth\\(requires: "${READ}"\\)`, 'g'))).toHaveLength(3);
115
+ expect(sdl.subscription).not.toContain(NARROW);
116
+ });
@@ -79,6 +79,19 @@ exports.foldedEntityFields = {
79
79
  `
80
80
  };
81
81
  exports.mainSubChannel = 'mainCH';
82
+ /**
83
+ * Every base operation generated by generateTypeDefs, in declaration order.
84
+ */
85
+ const baseMethodNames = [
86
+ 'count', 'list', 'get', 'findOne',
87
+ 'create', 'update', 'remove', 'copy',
88
+ 'addArrayItem', 'updateArrayItem', 'removeArrayItem',
89
+ 'bulkUpdate', 'bulkRemove'
90
+ ];
91
+ /**
92
+ * Base operations that default to the `read` roles; every other one defaults to `write`.
93
+ */
94
+ const readMethodNames = ['count', 'list', 'get', 'findOne'];
82
95
  /**
83
96
  *
84
97
  * @param modelName
@@ -119,24 +132,52 @@ function getSubscriptionNames(modelName) {
119
132
  };
120
133
  }
121
134
  /**
135
+ * Formats a role list as an `@auth` directive argument list.
136
+ *
137
+ * @param roles
138
+ * @returns `(requires: "ROLE_A,ROLE_B")`, or an empty string when no role is required
139
+ */
140
+ function toAuthDirective(roles) {
141
+ if (!roles)
142
+ return '';
143
+ return `(requires: "${Array.isArray(roles) ? roles.join(',') : roles}")`;
144
+ }
145
+ /**
146
+ * Resolves the `@auth` directive of every generated operation.
147
+ *
148
+ * Read operations default to `roles.read` and the remaining ones to `roles.write`; a per-operation
149
+ * entry in `roles.overrides` replaces that default for that operation only.
150
+ *
151
+ * IMPORTANT: an override is applied only when it carries roles. An entry whose value is empty or
152
+ * undefined is treated as ABSENT and the default stands, because the alternative fails OPEN: a
153
+ * mistyped constant, or one that did not resolve, would silently strip the directive and publish
154
+ * the operation to everyone. An operation meant to require no role is expressed by leaving the
155
+ * module's `roles` out, not by an empty override.
122
156
  *
123
157
  * @param extension
124
- * @returns
158
+ * @returns the directive suffix of each base operation, empty string when unconstrained
125
159
  */
126
160
  function getAuth(extension = {}) {
127
- const roles = { read: '', write: '' };
161
+ var _a;
162
+ const methods = {};
163
+ const auth = { read: '', write: '', methods };
164
+ for (const method of baseMethodNames)
165
+ methods[method] = '';
128
166
  if (!extension.roles)
129
- return roles;
167
+ return auth;
130
168
  if ((0, utils_1.isString)(extension.roles))
131
169
  extension.roles = extension.roles.split(',');
132
170
  if (Array.isArray(extension.roles))
133
171
  extension.roles = { read: extension.roles, write: extension.roles };
134
172
  const sr = extension.roles;
135
- if ((sr.read))
136
- roles.read = `(requires: "${Array.isArray(sr.read) ? sr.read.join(',') : sr.read}")`;
137
- if (sr.write)
138
- roles.write = `(requires: "${Array.isArray(sr.write) ? sr.write.join(',') : sr.write}")`;
139
- return roles;
173
+ auth.read = toAuthDirective(sr.read);
174
+ auth.write = toAuthDirective(sr.write);
175
+ for (const method of baseMethodNames) {
176
+ const override = (_a = sr.overrides) === null || _a === void 0 ? void 0 : _a[method];
177
+ const fallback = readMethodNames.includes(method) ? auth.read : auth.write;
178
+ methods[method] = (override === null || override === void 0 ? void 0 : override.length) ? toAuthDirective(override) : fallback;
179
+ }
180
+ return auth;
140
181
  }
141
182
  /**
142
183
  *
@@ -163,23 +204,23 @@ function generateTypeDefs(ctrl, schemaExtension = {}, mergeCodedEntity, readOnly
163
204
  dataFields = exports.codedEntityFields;
164
205
  const baseTypeDefs = {
165
206
  query: `
166
- ${methods.count}(params: ${modelName}Query) : Int! @auth${roles.read}
167
- ${methods.list}(params: ${modelName}Query, pagination: OffsetPagination): [${modelName}]! @auth${roles.read}
168
- ${methods.get}(id: ID!) : ${modelName}! @auth${roles.read}
169
- ${methods.findOne}(params: ${modelName}Query) : ${modelName} @auth${roles.read}
207
+ ${methods.count}(params: ${modelName}Query) : Int! @auth${roles.methods.count}
208
+ ${methods.list}(params: ${modelName}Query, pagination: OffsetPagination): [${modelName}]! @auth${roles.methods.list}
209
+ ${methods.get}(id: ID!) : ${modelName}! @auth${roles.methods.get}
210
+ ${methods.findOne}(params: ${modelName}Query) : ${modelName} @auth${roles.methods.findOne}
170
211
  `,
171
212
  mutation: `
172
- ${methods.create}(item: ${modelName}Input!) : ${modelName}! @auth${roles.write}
173
- ${methods.update}(id: ID!, item: ${modelName}Input!) : ${modelName}! @auth${roles.write}
174
- ${methods.remove}(id: ID!) : ${modelName}! @auth${roles.write}
175
- ${methods.copy}(id: ID!, item: ${modelName}Input!) : ${modelName}! @auth${roles.write}
213
+ ${methods.create}(item: ${modelName}Input!) : ${modelName}! @auth${roles.methods.create}
214
+ ${methods.update}(id: ID!, item: ${modelName}Input!) : ${modelName}! @auth${roles.methods.update}
215
+ ${methods.remove}(id: ID!) : ${modelName}! @auth${roles.methods.remove}
216
+ ${methods.copy}(id: ID!, item: ${modelName}Input!) : ${modelName}! @auth${roles.methods.copy}
176
217
 
177
- ${methods.addArrayItem}(id: ID!, item: AddArrayElemItem!) : ${modelName}! @auth${roles.write}
178
- ${methods.updateArrayItem}(id: ID!, item: UpdateArrayElemItem!) : ${modelName}! @auth${roles.write}
179
- ${methods.removeArrayItem}(id: ID!, item: RemoveArrayElemItem!) : ${modelName}! @auth${roles.write}
218
+ ${methods.addArrayItem}(id: ID!, item: AddArrayElemItem!) : ${modelName}! @auth${roles.methods.addArrayItem}
219
+ ${methods.updateArrayItem}(id: ID!, item: UpdateArrayElemItem!) : ${modelName}! @auth${roles.methods.updateArrayItem}
220
+ ${methods.removeArrayItem}(id: ID!, item: RemoveArrayElemItem!) : ${modelName}! @auth${roles.methods.removeArrayItem}
180
221
 
181
- ${methods.bulkUpdate}(ids: [ID]!, items: [${modelName}Input]!) : [${modelName}]! @auth${roles.write}
182
- ${methods.bulkRemove}(ids: [ID]!) : [${modelName}]! @auth${roles.write}
222
+ ${methods.bulkUpdate}(ids: [ID]!, items: [${modelName}Input]!) : [${modelName}]! @auth${roles.methods.bulkUpdate}
223
+ ${methods.bulkRemove}(ids: [ID]!) : [${modelName}]! @auth${roles.methods.bulkRemove}
183
224
  `,
184
225
  subscription: `
185
226
  ${subscriptions.create.id}(channelId: String!, filter: ${modelName}Query): ${modelName}! @auth${roles.read}
@@ -64,4 +64,28 @@ export interface NxGraphQLSchemaExtension extends NxGraphQLSchemaData {
64
64
  export interface NxGraphQLSchemaRole {
65
65
  read?: string | string[];
66
66
  write?: string | string[];
67
+ overrides?: NxGraphQLSchemaRoleOverrides;
68
+ }
69
+ /**
70
+ * Per-operation role overrides.
71
+ *
72
+ * Each key is one of the generated base methods (see NxGraphQLBaseMethodNames) and its value
73
+ * replaces the roles required by that single operation. Operations left out keep the default
74
+ * behaviour: `read` for count/list/get/findOne, `write` for every other operation.
75
+ *
76
+ * IMPORTANT: an empty or undefined value does NOT remove the role constraint, it falls back to
77
+ * the default. Publishing an operation without any constraint must stay an explicit choice made
78
+ * by omitting `read`/`write`, never an accident of a half-filled override map.
79
+ *
80
+ * Subscriptions are not overridable: they always follow `read`.
81
+ */
82
+ export type NxGraphQLSchemaRoleOverrides = Partial<Record<keyof NxGraphQLBaseMethodNames, string | string[]>>;
83
+ /**
84
+ * Resolved `@auth` directive suffixes, ready to be interpolated into the generated SDL.
85
+ * Each value is either an empty string (no role constraint) or `(requires: "ROLE_A,ROLE_B")`.
86
+ */
87
+ export interface NxGraphQLAuthDirectives {
88
+ read: string;
89
+ write: string;
90
+ methods: Record<keyof NxGraphQLBaseMethodNames, string>;
67
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextage/nx-frame-be",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -16,6 +16,9 @@
16
16
  "jest": {
17
17
  "preset": "ts-jest",
18
18
  "testEnvironment": "node",
19
+ "testPathIgnorePatterns": [
20
+ "<rootDir>/build/"
21
+ ],
19
22
  "setupFilesAfterEnv": [
20
23
  "./src/test/setup.ts"
21
24
  ]