zenstack 1.0.0-alpha.22 → 1.0.0-alpha.24

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 (85) hide show
  1. package/README.md +79 -9
  2. package/cli/cli-error.js +3 -5
  3. package/cli/cli-error.js.map +1 -1
  4. package/cli/cli-util.js +123 -105
  5. package/cli/cli-util.js.map +1 -1
  6. package/cli/index.js +105 -63
  7. package/cli/index.js.map +1 -1
  8. package/cli/plugin-runner.js +130 -127
  9. package/cli/plugin-runner.js.map +1 -1
  10. package/language-server/constants.js +5 -13
  11. package/language-server/constants.js.map +1 -1
  12. package/language-server/main.js +8 -15
  13. package/language-server/main.js.map +1 -1
  14. package/language-server/types.js +3 -1
  15. package/language-server/types.js.map +1 -1
  16. package/language-server/utils.js +13 -16
  17. package/language-server/utils.js.map +1 -1
  18. package/language-server/validator/attribute-validator.js +3 -7
  19. package/language-server/validator/attribute-validator.js.map +1 -1
  20. package/language-server/validator/datamodel-validator.js +293 -347
  21. package/language-server/validator/datamodel-validator.js.map +1 -1
  22. package/language-server/validator/datasource-validator.js +61 -71
  23. package/language-server/validator/datasource-validator.js.map +1 -1
  24. package/language-server/validator/enum-validator.js +6 -10
  25. package/language-server/validator/enum-validator.js.map +1 -1
  26. package/language-server/validator/expression-validator.js +25 -31
  27. package/language-server/validator/expression-validator.js.map +1 -1
  28. package/language-server/validator/schema-validator.js +18 -25
  29. package/language-server/validator/schema-validator.js.map +1 -1
  30. package/language-server/validator/utils.js +86 -85
  31. package/language-server/validator/utils.js.map +1 -1
  32. package/language-server/validator/zmodel-validator.js +55 -58
  33. package/language-server/validator/zmodel-validator.js.map +1 -1
  34. package/language-server/zmodel-formatter.js +40 -21
  35. package/language-server/zmodel-formatter.js.map +1 -1
  36. package/language-server/zmodel-linker.js +328 -331
  37. package/language-server/zmodel-linker.js.map +1 -1
  38. package/language-server/zmodel-module.js +50 -59
  39. package/language-server/zmodel-module.js.map +1 -1
  40. package/language-server/zmodel-scope.js +35 -25
  41. package/language-server/zmodel-scope.js.map +1 -1
  42. package/language-server/zmodel-workspace-manager.js +30 -18
  43. package/language-server/zmodel-workspace-manager.js.map +1 -1
  44. package/package.json +8 -12
  45. package/plugins/access-policy/expression-writer.js +301 -292
  46. package/plugins/access-policy/expression-writer.js.map +1 -1
  47. package/plugins/access-policy/index.js +20 -11
  48. package/plugins/access-policy/index.js.map +1 -1
  49. package/plugins/access-policy/policy-guard-generator.js +327 -321
  50. package/plugins/access-policy/policy-guard-generator.js.map +1 -1
  51. package/plugins/access-policy/typescript-expression-transformer.js +94 -95
  52. package/plugins/access-policy/typescript-expression-transformer.js.map +1 -1
  53. package/plugins/access-policy/utils.js +7 -9
  54. package/plugins/access-policy/utils.js.map +1 -1
  55. package/plugins/access-policy/zod-schema-generator.js +143 -159
  56. package/plugins/access-policy/zod-schema-generator.js.map +1 -1
  57. package/plugins/model-meta/index.js +97 -102
  58. package/plugins/model-meta/index.js.map +1 -1
  59. package/plugins/plugin-utils.js +34 -40
  60. package/plugins/plugin-utils.js.map +1 -1
  61. package/plugins/prisma/indent-string.js +4 -8
  62. package/plugins/prisma/indent-string.js.map +1 -1
  63. package/plugins/prisma/index.js +20 -11
  64. package/plugins/prisma/index.js.map +1 -1
  65. package/plugins/prisma/prisma-builder.js +235 -213
  66. package/plugins/prisma/prisma-builder.js.map +1 -1
  67. package/plugins/prisma/schema-generator.js +205 -186
  68. package/plugins/prisma/schema-generator.js.map +1 -1
  69. package/plugins/prisma/zmodel-code-generator.d.ts +2 -1
  70. package/plugins/prisma/zmodel-code-generator.js +109 -105
  71. package/plugins/prisma/zmodel-code-generator.js.map +1 -1
  72. package/telemetry.js +107 -90
  73. package/telemetry.js.map +1 -1
  74. package/types.js +3 -1
  75. package/types.js.map +1 -1
  76. package/utils/ast-utils.js +67 -67
  77. package/utils/ast-utils.js.map +1 -1
  78. package/utils/exec-utils.js +6 -15
  79. package/utils/exec-utils.js.map +1 -1
  80. package/utils/pkg-utils.js +38 -35
  81. package/utils/pkg-utils.js.map +1 -1
  82. package/utils/version-utils.js +9 -10
  83. package/utils/version-utils.js.map +1 -1
  84. package/global.d.js +0 -1
  85. package/global.d.js.map +0 -1
@@ -1,345 +1,351 @@
1
1
  "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = void 0;
7
- var _ast = require("@zenstackhq/language/ast");
8
- var _sdk = require("@zenstackhq/sdk");
9
- var _changeCase = require("change-case");
10
- var _langium = require("langium");
11
- var _path = _interopRequireDefault(require("path"));
12
- var _tsMorph = require("ts-morph");
13
- var _ = require(".");
14
- var _utils = require("../../language-server/utils");
15
- var _astUtils = require("../../utils/ast-utils");
16
- var _pluginUtils = require("../plugin-utils");
17
- var _expressionWriter = require("./expression-writer");
18
- var _utils2 = require("./utils");
19
- var _zodSchemaGenerator = require("./zod-schema-generator");
20
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const ast_1 = require("@zenstackhq/language/ast");
16
+ const sdk_1 = require("@zenstackhq/sdk");
17
+ const change_case_1 = require("change-case");
18
+ const langium_1 = require("langium");
19
+ const path_1 = __importDefault(require("path"));
20
+ const ts_morph_1 = require("ts-morph");
21
+ const _1 = require(".");
22
+ const utils_1 = require("../../language-server/utils");
23
+ const ast_utils_1 = require("../../utils/ast-utils");
24
+ const plugin_utils_1 = require("../plugin-utils");
25
+ const expression_writer_1 = require("./expression-writer");
26
+ const utils_2 = require("./utils");
27
+ const zod_schema_generator_1 = require("./zod-schema-generator");
21
28
  const UNKNOWN_USER_ID = 'zenstack_unknown_user';
22
-
23
29
  /**
24
30
  * Generates source file that contains Prisma query guard objects used for injecting database queries
25
31
  */
26
32
  class PolicyGenerator {
27
- async generate(model, options) {
28
- const output = options.output ? options.output : (0, _pluginUtils.getDefaultOutputFolder)();
29
- if (!output) {
30
- console.error(`Unable to determine output path, not running plugin ${_.name}`);
31
- return;
32
- }
33
- const project = new _tsMorph.Project();
34
- const sf = project.createSourceFile(_path.default.join(output, 'policy.ts'), undefined, {
35
- overwrite: true
36
- });
37
- sf.addImportDeclaration({
38
- namedImports: [{
39
- name: 'QueryContext'
40
- }],
41
- moduleSpecifier: `${_pluginUtils.RUNTIME_PACKAGE}`,
42
- isTypeOnly: true
43
- });
44
- sf.addImportDeclaration({
45
- namedImports: [{
46
- name: 'z'
47
- }],
48
- moduleSpecifier: 'zod'
49
- });
50
-
51
- // import enums
52
- for (const e of model.declarations.filter(d => (0, _ast.isEnum)(d))) {
53
- sf.addImportDeclaration({
54
- namedImports: [{
55
- name: e.name
56
- }],
57
- moduleSpecifier: '@prisma/client'
58
- });
59
- }
60
- const models = model.declarations.filter(d => (0, _ast.isDataModel)(d));
61
- const policyMap = {};
62
- for (const model of models) {
63
- policyMap[model.name] = await this.generateQueryGuardForModel(model, sf);
64
- }
65
- const zodGenerator = new _zodSchemaGenerator.ZodSchemaGenerator();
66
- sf.addVariableStatement({
67
- declarationKind: _tsMorph.VariableDeclarationKind.Const,
68
- declarations: [{
69
- name: 'policy',
70
- initializer: writer => {
71
- writer.block(() => {
72
- writer.write('guard:');
73
- writer.inlineBlock(() => {
74
- for (const [model, map] of Object.entries(policyMap)) {
75
- writer.write(`${(0, _changeCase.camelCase)(model)}:`);
76
- writer.inlineBlock(() => {
77
- for (const [op, func] of Object.entries(map)) {
78
- if (typeof func === 'object') {
79
- writer.write(`${op}: ${JSON.stringify(func)},`);
80
- } else {
81
- writer.write(`${op}: ${func},`);
82
- }
83
- }
33
+ generate(model, options) {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ const output = options.output ? options.output : (0, plugin_utils_1.getDefaultOutputFolder)();
36
+ if (!output) {
37
+ console.error(`Unable to determine output path, not running plugin ${_1.name}`);
38
+ return;
39
+ }
40
+ const project = new ts_morph_1.Project();
41
+ const sf = project.createSourceFile(path_1.default.join(output, 'policy.ts'), undefined, { overwrite: true });
42
+ sf.addImportDeclaration({
43
+ namedImports: [{ name: 'QueryContext' }],
44
+ moduleSpecifier: `${plugin_utils_1.RUNTIME_PACKAGE}`,
45
+ isTypeOnly: true,
46
+ });
47
+ sf.addImportDeclaration({
48
+ namedImports: [{ name: 'z' }],
49
+ moduleSpecifier: 'zod',
50
+ });
51
+ // import enums
52
+ for (const e of model.declarations.filter((d) => (0, ast_1.isEnum)(d))) {
53
+ sf.addImportDeclaration({
54
+ namedImports: [{ name: e.name }],
55
+ moduleSpecifier: '@prisma/client',
84
56
  });
85
- writer.write(',');
86
- }
57
+ }
58
+ const models = model.declarations.filter((d) => (0, ast_1.isDataModel)(d));
59
+ const policyMap = {};
60
+ for (const model of models) {
61
+ policyMap[model.name] = yield this.generateQueryGuardForModel(model, sf);
62
+ }
63
+ const zodGenerator = new zod_schema_generator_1.ZodSchemaGenerator();
64
+ sf.addVariableStatement({
65
+ declarationKind: ts_morph_1.VariableDeclarationKind.Const,
66
+ declarations: [
67
+ {
68
+ name: 'policy',
69
+ initializer: (writer) => {
70
+ writer.block(() => {
71
+ writer.write('guard:');
72
+ writer.inlineBlock(() => {
73
+ for (const [model, map] of Object.entries(policyMap)) {
74
+ writer.write(`${(0, change_case_1.camelCase)(model)}:`);
75
+ writer.inlineBlock(() => {
76
+ for (const [op, func] of Object.entries(map)) {
77
+ if (typeof func === 'object') {
78
+ writer.write(`${op}: ${JSON.stringify(func)},`);
79
+ }
80
+ else {
81
+ writer.write(`${op}: ${func},`);
82
+ }
83
+ }
84
+ });
85
+ writer.write(',');
86
+ }
87
+ });
88
+ writer.writeLine(',');
89
+ writer.write('schema:');
90
+ zodGenerator.generate(writer, models);
91
+ });
92
+ },
93
+ },
94
+ ],
87
95
  });
88
- writer.writeLine(',');
89
- writer.write('schema:');
90
- zodGenerator.generate(writer, models);
91
- });
92
- }
93
- }]
94
- });
95
- sf.addStatements('export default policy');
96
- sf.formatText();
97
- await project.save();
98
- await project.emit();
99
- }
100
- getPolicyExpressions(model, kind, operation) {
101
- const attrs = model.attributes.filter(attr => {
102
- var _attr$decl$ref;
103
- return ((_attr$decl$ref = attr.decl.ref) === null || _attr$decl$ref === void 0 ? void 0 : _attr$decl$ref.name) === `@@${kind}`;
104
- });
105
- const checkOperation = operation === 'postUpdate' ? 'update' : operation;
106
- let result = attrs.filter(attr => {
107
- const opsValue = (0, _sdk.getLiteral)(attr.args[0].value);
108
- if (!opsValue) {
109
- return false;
110
- }
111
- const ops = opsValue.split(',').map(s => s.trim());
112
- return ops.includes(checkOperation) || ops.includes('all');
113
- }).map(attr => attr.args[1].value);
114
- if (operation === 'update') {
115
- result = this.processUpdatePolicies(result, false);
116
- } else if (operation === 'postUpdate') {
117
- result = this.processUpdatePolicies(result, true);
118
- }
119
- return result;
120
- }
121
- processUpdatePolicies(expressions, postUpdate) {
122
- return expressions.map(expr => this.visitPolicyExpression(expr, postUpdate)).filter(e => !!e);
123
- }
124
- visitPolicyExpression(expr, postUpdate) {
125
- if ((0, _ast.isBinaryExpr)(expr) && (expr.operator === '&&' || expr.operator === '||')) {
126
- const left = this.visitPolicyExpression(expr.left, postUpdate);
127
- const right = this.visitPolicyExpression(expr.right, postUpdate);
128
- if (!left) return right;
129
- if (!right) return left;
130
- return {
131
- ...expr,
132
- left,
133
- right
134
- };
135
- }
136
- if ((0, _ast.isUnaryExpr)(expr) && expr.operator === '!') {
137
- const operand = this.visitPolicyExpression(expr.operand, postUpdate);
138
- if (!operand) return undefined;
139
- return {
140
- ...expr,
141
- operand
142
- };
143
- }
144
- if (postUpdate && !this.hasFutureReference(expr)) {
145
- return undefined;
146
- } else if (!postUpdate && this.hasFutureReference(expr)) {
147
- return undefined;
148
- }
149
- return expr;
150
- }
151
- hasFutureReference(expr) {
152
- for (const node of (0, _langium.streamAllContents)(expr)) {
153
- var _node$function$ref;
154
- if ((0, _ast.isInvocationExpr)(node) && ((_node$function$ref = node.function.ref) === null || _node$function$ref === void 0 ? void 0 : _node$function$ref.name) === 'future' && (0, _utils.isFromStdlib)(node.function.ref)) {
155
- return true;
156
- }
96
+ sf.addStatements('export default policy');
97
+ sf.formatText();
98
+ yield project.save();
99
+ yield project.emit();
100
+ });
157
101
  }
158
- return false;
159
- }
160
- async generateQueryGuardForModel(model, sourceFile) {
161
- const result = {};
162
-
163
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
- const policies = (0, _astUtils.analyzePolicies)(model);
165
- for (const kind of _pluginUtils.ALL_OPERATION_KINDS) {
166
- if (policies[kind] === true || policies[kind] === false) {
167
- result[kind] = policies[kind];
168
- continue;
169
- }
170
- const denies = this.getPolicyExpressions(model, 'deny', kind);
171
- const allows = this.getPolicyExpressions(model, 'allow', kind);
172
- if (kind === 'update' && allows.length === 0) {
173
- // no allow rule for 'update', policy is constant based on if there's
174
- // post-update counterpart
175
- if (this.getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) {
176
- result[kind] = false;
177
- continue;
178
- } else {
179
- result[kind] = true;
180
- continue;
102
+ getPolicyExpressions(model, kind, operation) {
103
+ const attrs = model.attributes.filter((attr) => { var _a; return ((_a = attr.decl.ref) === null || _a === void 0 ? void 0 : _a.name) === `@@${kind}`; });
104
+ const checkOperation = operation === 'postUpdate' ? 'update' : operation;
105
+ let result = attrs
106
+ .filter((attr) => {
107
+ const opsValue = (0, sdk_1.getLiteral)(attr.args[0].value);
108
+ if (!opsValue) {
109
+ return false;
110
+ }
111
+ const ops = opsValue.split(',').map((s) => s.trim());
112
+ return ops.includes(checkOperation) || ops.includes('all');
113
+ })
114
+ .map((attr) => attr.args[1].value);
115
+ if (operation === 'update') {
116
+ result = this.processUpdatePolicies(result, false);
181
117
  }
182
- }
183
- if (kind === 'postUpdate' && allows.length === 0 && denies.length === 0) {
184
- // no rule 'postUpdate', always allow
185
- result[kind] = true;
186
- continue;
187
- }
188
- const func = this.generateQueryGuardFunction(sourceFile, model, kind, allows, denies);
189
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
190
- result[kind] = func.getName();
191
- if (kind === 'postUpdate') {
192
- const preValueSelect = this.generatePreValueSelect(model, allows, denies);
193
- if (preValueSelect) {
194
- result['preValueSelect'] = preValueSelect;
118
+ else if (operation === 'postUpdate') {
119
+ result = this.processUpdatePolicies(result, true);
195
120
  }
196
- }
121
+ return result;
197
122
  }
198
- return result;
199
- }
200
-
201
- // generates an object that can be used as the 'select' argument when fetching pre-update
202
- // entity value
203
- generatePreValueSelect(model, allows, denies) {
204
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
- const result = {};
206
- const addPath = path => {
207
- let curr = result;
208
- path.forEach((seg, i) => {
209
- if (i === path.length - 1) {
210
- curr[seg] = true;
211
- } else {
212
- if (!curr[seg]) {
213
- curr[seg] = {
214
- select: {}
215
- };
216
- }
217
- curr = curr[seg].select;
218
- }
219
- });
220
- };
221
- const visit = node => {
222
- if ((0, _ast.isReferenceExpr)(node)) {
223
- const target = (0, _sdk.resolved)(node.target);
224
- if ((0, _ast.isDataModelField)(target)) {
225
- // a field selection, it's a terminal
226
- return [target.name];
227
- }
228
- } else if ((0, _ast.isMemberAccessExpr)(node)) {
229
- if ((0, _utils2.isFutureExpr)(node.operand)) {
230
- // future().field is not subject to pre-update select
231
- return undefined;
232
- }
233
-
234
- // build a selection path inside-out for chained member access
235
- const inner = visit(node.operand);
236
- if (inner) {
237
- return [...inner, node.member.$refText];
123
+ processUpdatePolicies(expressions, postUpdate) {
124
+ return expressions
125
+ .map((expr) => this.visitPolicyExpression(expr, postUpdate))
126
+ .filter((e) => !!e);
127
+ }
128
+ visitPolicyExpression(expr, postUpdate) {
129
+ if ((0, ast_1.isBinaryExpr)(expr) && (expr.operator === '&&' || expr.operator === '||')) {
130
+ const left = this.visitPolicyExpression(expr.left, postUpdate);
131
+ const right = this.visitPolicyExpression(expr.right, postUpdate);
132
+ if (!left)
133
+ return right;
134
+ if (!right)
135
+ return left;
136
+ return Object.assign(Object.assign({}, expr), { left, right });
238
137
  }
239
- }
240
- return undefined;
241
- };
242
- for (const rule of [...allows, ...denies]) {
243
- for (const expr of (0, _langium.streamAllContents)(rule).filter(node => (0, _ast.isExpression)(node))) {
244
- // only care about member access and reference expressions
245
- if (!(0, _ast.isMemberAccessExpr)(expr) && !(0, _ast.isReferenceExpr)(expr)) {
246
- continue;
138
+ if ((0, ast_1.isUnaryExpr)(expr) && expr.operator === '!') {
139
+ const operand = this.visitPolicyExpression(expr.operand, postUpdate);
140
+ if (!operand)
141
+ return undefined;
142
+ return Object.assign(Object.assign({}, expr), { operand });
247
143
  }
248
- if (expr.$container.$type === _ast.MemberAccessExpr) {
249
- // only visit top-level member access
250
- continue;
144
+ if (postUpdate && !this.hasFutureReference(expr)) {
145
+ return undefined;
251
146
  }
252
- const path = visit(expr);
253
- if (path) {
254
- var _expr$$resolvedType;
255
- if ((0, _ast.isDataModel)((_expr$$resolvedType = expr.$resolvedType) === null || _expr$$resolvedType === void 0 ? void 0 : _expr$$resolvedType.decl)) {
256
- // member selection ended at a data model field, include its 'id'
257
- path.push('id');
258
- }
259
- addPath(path);
147
+ else if (!postUpdate && this.hasFutureReference(expr)) {
148
+ return undefined;
260
149
  }
261
- }
150
+ return expr;
262
151
  }
263
- return Object.keys(result).length === 0 ? null : result;
264
- }
265
- generateQueryGuardFunction(sourceFile, model, kind, allows, denies) {
266
- const func = sourceFile.addFunction({
267
- name: model.name + '_' + kind,
268
- returnType: 'any',
269
- parameters: [{
270
- name: 'context',
271
- type: 'QueryContext'
272
- }]
273
- }).addBody();
274
-
275
- // check if any allow or deny rule contains 'auth()' invocation
276
- let hasAuthRef = false;
277
- for (const node of [...denies, ...allows]) {
278
- for (const child of (0, _langium.streamAllContents)(node)) {
279
- if ((0, _ast.isInvocationExpr)(child) && (0, _sdk.resolved)(child.function).name === 'auth') {
280
- hasAuthRef = true;
281
- break;
152
+ hasFutureReference(expr) {
153
+ var _a;
154
+ for (const node of (0, langium_1.streamAllContents)(expr)) {
155
+ if ((0, ast_1.isInvocationExpr)(node) && ((_a = node.function.ref) === null || _a === void 0 ? void 0 : _a.name) === 'future' && (0, utils_1.isFromStdlib)(node.function.ref)) {
156
+ return true;
157
+ }
282
158
  }
283
- }
284
- if (hasAuthRef) {
285
- break;
286
- }
287
- }
288
- if (hasAuthRef) {
289
- const userModel = model.$container.declarations.find(decl => (0, _ast.isDataModel)(decl) && decl.name === 'User');
290
- if (!userModel) {
291
- throw new _sdk.PluginError('User model not found');
292
- }
293
- const userIdField = (0, _astUtils.getIdField)(userModel);
294
- if (!userIdField) {
295
- throw new _sdk.PluginError('User model does not have an id field');
296
- }
297
- func.addStatements(
298
- // make sure user id is always available
299
- `const user = context.user?.${userIdField.name} ? context.user : { ...context.user, ${userIdField.name}: '${UNKNOWN_USER_ID}' };`);
159
+ return false;
300
160
  }
301
-
302
- // r = <guard object>;
303
- func.addStatements(writer => {
304
- writer.write('return ');
305
- const exprWriter = new _expressionWriter.ExpressionWriter(writer, kind === 'postUpdate');
306
- const writeDenies = () => {
307
- writer.conditionalWrite(denies.length > 1, '{ AND: [');
308
- denies.forEach((expr, i) => {
309
- writer.inlineBlock(() => {
310
- writer.write('NOT: ');
311
- exprWriter.write(expr);
312
- });
313
- writer.conditionalWrite(i !== denies.length - 1, ',');
161
+ generateQueryGuardForModel(model, sourceFile) {
162
+ return __awaiter(this, void 0, void 0, function* () {
163
+ const result = {};
164
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
165
+ const policies = (0, ast_utils_1.analyzePolicies)(model);
166
+ for (const kind of plugin_utils_1.ALL_OPERATION_KINDS) {
167
+ if (policies[kind] === true || policies[kind] === false) {
168
+ result[kind] = policies[kind];
169
+ continue;
170
+ }
171
+ const denies = this.getPolicyExpressions(model, 'deny', kind);
172
+ const allows = this.getPolicyExpressions(model, 'allow', kind);
173
+ if (kind === 'update' && allows.length === 0) {
174
+ // no allow rule for 'update', policy is constant based on if there's
175
+ // post-update counterpart
176
+ if (this.getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) {
177
+ result[kind] = false;
178
+ continue;
179
+ }
180
+ else {
181
+ result[kind] = true;
182
+ continue;
183
+ }
184
+ }
185
+ if (kind === 'postUpdate' && allows.length === 0 && denies.length === 0) {
186
+ // no rule 'postUpdate', always allow
187
+ result[kind] = true;
188
+ continue;
189
+ }
190
+ const func = this.generateQueryGuardFunction(sourceFile, model, kind, allows, denies);
191
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
192
+ result[kind] = func.getName();
193
+ if (kind === 'postUpdate') {
194
+ const preValueSelect = this.generatePreValueSelect(model, allows, denies);
195
+ if (preValueSelect) {
196
+ result['preValueSelect'] = preValueSelect;
197
+ }
198
+ }
199
+ }
200
+ return result;
314
201
  });
315
- writer.conditionalWrite(denies.length > 1, ']}');
316
- };
317
- const writeAllows = () => {
318
- writer.conditionalWrite(allows.length > 1, '{ OR: [');
319
- allows.forEach((expr, i) => {
320
- exprWriter.write(expr);
321
- writer.conditionalWrite(i !== allows.length - 1, ',');
202
+ }
203
+ // generates an object that can be used as the 'select' argument when fetching pre-update
204
+ // entity value
205
+ generatePreValueSelect(model, allows, denies) {
206
+ var _a;
207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
+ const result = {};
209
+ const addPath = (path) => {
210
+ let curr = result;
211
+ path.forEach((seg, i) => {
212
+ if (i === path.length - 1) {
213
+ curr[seg] = true;
214
+ }
215
+ else {
216
+ if (!curr[seg]) {
217
+ curr[seg] = { select: {} };
218
+ }
219
+ curr = curr[seg].select;
220
+ }
221
+ });
222
+ };
223
+ const visit = (node) => {
224
+ if ((0, ast_1.isReferenceExpr)(node)) {
225
+ const target = (0, sdk_1.resolved)(node.target);
226
+ if ((0, ast_1.isDataModelField)(target)) {
227
+ // a field selection, it's a terminal
228
+ return [target.name];
229
+ }
230
+ }
231
+ else if ((0, ast_1.isMemberAccessExpr)(node)) {
232
+ if ((0, utils_2.isFutureExpr)(node.operand)) {
233
+ // future().field is not subject to pre-update select
234
+ return undefined;
235
+ }
236
+ // build a selection path inside-out for chained member access
237
+ const inner = visit(node.operand);
238
+ if (inner) {
239
+ return [...inner, node.member.$refText];
240
+ }
241
+ }
242
+ return undefined;
243
+ };
244
+ for (const rule of [...allows, ...denies]) {
245
+ for (const expr of (0, langium_1.streamAllContents)(rule).filter((node) => (0, ast_1.isExpression)(node))) {
246
+ // only care about member access and reference expressions
247
+ if (!(0, ast_1.isMemberAccessExpr)(expr) && !(0, ast_1.isReferenceExpr)(expr)) {
248
+ continue;
249
+ }
250
+ if (expr.$container.$type === ast_1.MemberAccessExpr) {
251
+ // only visit top-level member access
252
+ continue;
253
+ }
254
+ const path = visit(expr);
255
+ if (path) {
256
+ if ((0, ast_1.isDataModel)((_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) {
257
+ // member selection ended at a data model field, include its 'id'
258
+ path.push('id');
259
+ }
260
+ addPath(path);
261
+ }
262
+ }
263
+ }
264
+ return Object.keys(result).length === 0 ? null : result;
265
+ }
266
+ generateQueryGuardFunction(sourceFile, model, kind, allows, denies) {
267
+ const func = sourceFile
268
+ .addFunction({
269
+ name: model.name + '_' + kind,
270
+ returnType: 'any',
271
+ parameters: [
272
+ {
273
+ name: 'context',
274
+ type: 'QueryContext',
275
+ },
276
+ ],
277
+ })
278
+ .addBody();
279
+ // check if any allow or deny rule contains 'auth()' invocation
280
+ let hasAuthRef = false;
281
+ for (const node of [...denies, ...allows]) {
282
+ for (const child of (0, langium_1.streamAllContents)(node)) {
283
+ if ((0, ast_1.isInvocationExpr)(child) && (0, sdk_1.resolved)(child.function).name === 'auth') {
284
+ hasAuthRef = true;
285
+ break;
286
+ }
287
+ }
288
+ if (hasAuthRef) {
289
+ break;
290
+ }
291
+ }
292
+ if (hasAuthRef) {
293
+ const userModel = model.$container.declarations.find((decl) => (0, ast_1.isDataModel)(decl) && decl.name === 'User');
294
+ if (!userModel) {
295
+ throw new sdk_1.PluginError('User model not found');
296
+ }
297
+ const userIdField = (0, ast_utils_1.getIdField)(userModel);
298
+ if (!userIdField) {
299
+ throw new sdk_1.PluginError('User model does not have an id field');
300
+ }
301
+ func.addStatements(
302
+ // make sure user id is always available
303
+ `const user = context.user?.${userIdField.name} ? context.user : { ...context.user, ${userIdField.name}: '${UNKNOWN_USER_ID}' };`);
304
+ }
305
+ // r = <guard object>;
306
+ func.addStatements((writer) => {
307
+ writer.write('return ');
308
+ const exprWriter = new expression_writer_1.ExpressionWriter(writer, kind === 'postUpdate');
309
+ const writeDenies = () => {
310
+ writer.conditionalWrite(denies.length > 1, '{ AND: [');
311
+ denies.forEach((expr, i) => {
312
+ writer.inlineBlock(() => {
313
+ writer.write('NOT: ');
314
+ exprWriter.write(expr);
315
+ });
316
+ writer.conditionalWrite(i !== denies.length - 1, ',');
317
+ });
318
+ writer.conditionalWrite(denies.length > 1, ']}');
319
+ };
320
+ const writeAllows = () => {
321
+ writer.conditionalWrite(allows.length > 1, '{ OR: [');
322
+ allows.forEach((expr, i) => {
323
+ exprWriter.write(expr);
324
+ writer.conditionalWrite(i !== allows.length - 1, ',');
325
+ });
326
+ writer.conditionalWrite(allows.length > 1, ']}');
327
+ };
328
+ if (allows.length > 0 && denies.length > 0) {
329
+ writer.write('{ AND: [');
330
+ writeDenies();
331
+ writer.write(',');
332
+ writeAllows();
333
+ writer.write(']}');
334
+ }
335
+ else if (denies.length > 0) {
336
+ writeDenies();
337
+ }
338
+ else if (allows.length > 0) {
339
+ writeAllows();
340
+ }
341
+ else {
342
+ // disallow any operation
343
+ writer.write(`{ ${sdk_1.GUARD_FIELD_NAME}: false }`);
344
+ }
345
+ writer.write(';');
322
346
  });
323
- writer.conditionalWrite(allows.length > 1, ']}');
324
- };
325
- if (allows.length > 0 && denies.length > 0) {
326
- writer.write('{ AND: [');
327
- writeDenies();
328
- writer.write(',');
329
- writeAllows();
330
- writer.write(']}');
331
- } else if (denies.length > 0) {
332
- writeDenies();
333
- } else if (allows.length > 0) {
334
- writeAllows();
335
- } else {
336
- // disallow any operation
337
- writer.write(`{ ${_sdk.GUARD_FIELD_NAME}: false }`);
338
- }
339
- writer.write(';');
340
- });
341
- return func;
342
- }
347
+ return func;
348
+ }
343
349
  }
344
350
  exports.default = PolicyGenerator;
345
351
  //# sourceMappingURL=policy-guard-generator.js.map