zenstack 2.1.2 → 2.2.1

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.
@@ -14,60 +14,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.PolicyGenerator = void 0;
16
16
  const ast_1 = require("@zenstackhq/language/ast");
17
- const runtime_1 = require("@zenstackhq/runtime");
18
17
  const sdk_1 = require("@zenstackhq/sdk");
19
18
  const prisma_1 = require("@zenstackhq/sdk/prisma");
20
19
  const langium_1 = require("langium");
21
20
  const lower_case_first_1 = require("lower-case-first");
22
21
  const path_1 = __importDefault(require("path"));
23
22
  const ts_morph_1 = require("ts-morph");
24
- const __1 = require("..");
25
- const ast_utils_1 = require("../../../utils/ast-utils");
26
- const plugin_utils_1 = require("../../plugin-utils");
27
23
  const constraint_transformer_1 = require("./constraint-transformer");
28
- const expression_writer_1 = require("./expression-writer");
24
+ const utils_1 = require("./utils");
29
25
  /**
30
26
  * Generates source file that contains Prisma query guard objects used for injecting database queries
31
27
  */
32
28
  class PolicyGenerator {
33
- generate(project, model, options, output) {
29
+ constructor(options) {
30
+ this.options = options;
31
+ }
32
+ generate(project, model, output) {
34
33
  return __awaiter(this, void 0, void 0, function* () {
35
34
  const sf = project.createSourceFile(path_1.default.join(output, 'policy.ts'), undefined, { overwrite: true });
36
35
  sf.addStatements('/* eslint-disable */');
37
- sf.addImportDeclaration({
38
- namedImports: [
39
- { name: 'type QueryContext' },
40
- { name: 'type CrudContract' },
41
- { name: 'allFieldsEqual' },
42
- { name: 'type PolicyDef' },
43
- { name: 'type CheckerContext' },
44
- { name: 'type CheckerConstraint' },
45
- ],
46
- moduleSpecifier: `${sdk_1.RUNTIME_PACKAGE}`,
47
- });
48
- // import enums
49
- const prismaImport = (0, prisma_1.getPrismaClientImportSpec)(output, options);
50
- for (const e of model.declarations.filter((d) => (0, ast_1.isEnum)(d) && this.isEnumReferenced(model, d))) {
51
- sf.addImportDeclaration({
52
- namedImports: [{ name: e.name }],
53
- moduleSpecifier: prismaImport,
54
- });
55
- }
36
+ this.writeImports(model, output, sf);
56
37
  const models = (0, sdk_1.getDataModels)(model);
57
- // policy guard functions
58
- const policyMap = {};
59
- for (const model of models) {
60
- policyMap[model.name] = yield this.generateQueryGuardForModel(model, sf);
61
- }
62
- const generatePermissionChecker = options.generatePermissionChecker === true;
63
- // CRUD checker functions
64
- const checkerMap = {};
65
- if (generatePermissionChecker) {
66
- for (const model of models) {
67
- checkerMap[model.name] = yield this.generateCheckerForModel(model, sf);
68
- }
69
- }
70
- const authSelector = this.generateAuthSelector(models);
71
38
  sf.addVariableStatement({
72
39
  declarationKind: ts_morph_1.VariableDeclarationKind.Const,
73
40
  declarations: [
@@ -76,53 +43,9 @@ class PolicyGenerator {
76
43
  type: 'PolicyDef',
77
44
  initializer: (writer) => {
78
45
  writer.block(() => {
79
- writer.write('guard:');
80
- writer.inlineBlock(() => {
81
- for (const [model, map] of Object.entries(policyMap)) {
82
- writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model)}:`);
83
- writer.inlineBlock(() => {
84
- for (const [op, func] of Object.entries(map)) {
85
- if (typeof func === 'object') {
86
- writer.write(`${op}: ${JSON.stringify(func)},`);
87
- }
88
- else {
89
- writer.write(`${op}: ${func},`);
90
- }
91
- }
92
- });
93
- writer.write(',');
94
- }
95
- });
96
- writer.writeLine(',');
97
- if (generatePermissionChecker) {
98
- writer.write('checker:');
99
- writer.inlineBlock(() => {
100
- for (const [model, map] of Object.entries(checkerMap)) {
101
- writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model)}:`);
102
- writer.inlineBlock(() => {
103
- Object.entries(map).forEach(([op, func]) => {
104
- writer.write(`${op}: ${func},`);
105
- });
106
- });
107
- writer.writeLine(',');
108
- }
109
- });
110
- writer.writeLine(',');
111
- }
112
- writer.write('validation:');
113
- writer.inlineBlock(() => {
114
- for (const model of models) {
115
- writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model.name)}:`);
116
- writer.inlineBlock(() => {
117
- writer.write(`hasValidation: ${(0, sdk_1.hasValidationAttributes)(model)}`);
118
- });
119
- writer.writeLine(',');
120
- }
121
- });
122
- if (authSelector) {
123
- writer.writeLine(',');
124
- writer.write(`authSelector: ${JSON.stringify(authSelector)}`);
125
- }
46
+ this.writePolicy(writer, models, sf);
47
+ this.writeValidationMeta(writer, models);
48
+ this.writeAuthSelector(models, writer);
126
49
  });
127
50
  },
128
51
  },
@@ -130,200 +53,138 @@ class PolicyGenerator {
130
53
  });
131
54
  sf.addStatements('export default policy');
132
55
  // save ts files if requested explicitly or the user provided
133
- const preserveTsFiles = options.preserveTsFiles === true || !!options.output;
56
+ const preserveTsFiles = this.options.preserveTsFiles === true || !!this.options.output;
134
57
  if (preserveTsFiles) {
135
58
  yield sf.save();
136
59
  }
137
60
  });
138
61
  }
139
- // Generates a { select: ... } object to select `auth()` fields used in policy rules
140
- generateAuthSelector(models) {
141
- const authRules = [];
142
- models.forEach((model) => {
143
- // model-level rules
144
- const modelPolicyAttrs = model.attributes.filter((attr) => ['@@allow', '@@deny'].includes(attr.decl.$refText));
145
- // field-level rules
146
- const fieldPolicyAttrs = model.fields
147
- .flatMap((f) => f.attributes)
148
- .filter((attr) => ['@allow', '@deny'].includes(attr.decl.$refText));
149
- // all rule expression
150
- const allExpressions = [...modelPolicyAttrs, ...fieldPolicyAttrs]
151
- .filter((attr) => attr.args.length > 1)
152
- .map((attr) => attr.args[1].value);
153
- // collect `auth()` member access
154
- allExpressions.forEach((rule) => {
155
- (0, langium_1.streamAst)(rule).forEach((node) => {
156
- if ((0, ast_1.isMemberAccessExpr)(node) && (0, sdk_1.isAuthInvocation)(node.operand)) {
157
- authRules.push(node);
158
- }
159
- });
160
- });
62
+ writeImports(model, output, sf) {
63
+ sf.addImportDeclaration({
64
+ namedImports: [
65
+ { name: 'type QueryContext' },
66
+ { name: 'type CrudContract' },
67
+ { name: 'allFieldsEqual' },
68
+ { name: 'type PolicyDef' },
69
+ { name: 'type PermissionCheckerContext' },
70
+ { name: 'type PermissionCheckerConstraint' },
71
+ ],
72
+ moduleSpecifier: `${sdk_1.RUNTIME_PACKAGE}`,
161
73
  });
162
- if (authRules.length > 0) {
163
- return this.generateSelectForRules(authRules, true);
164
- }
165
- else {
166
- return undefined;
74
+ // import enums
75
+ const prismaImport = (0, prisma_1.getPrismaClientImportSpec)(output, this.options);
76
+ for (const e of model.declarations.filter((d) => (0, ast_1.isEnum)(d) && (0, utils_1.isEnumReferenced)(model, d))) {
77
+ sf.addImportDeclaration({
78
+ namedImports: [{ name: e.name }],
79
+ moduleSpecifier: prismaImport,
80
+ });
167
81
  }
168
82
  }
169
- isEnumReferenced(model, decl) {
170
- return (0, langium_1.streamAllContents)(model).some((node) => {
171
- var _a, _b;
172
- if ((0, ast_1.isDataModelField)(node) && ((_a = node.type.reference) === null || _a === void 0 ? void 0 : _a.ref) === decl) {
173
- // referenced as field type
174
- return true;
175
- }
176
- if ((0, sdk_1.isEnumFieldReference)(node) && ((_b = node.target.ref) === null || _b === void 0 ? void 0 : _b.$container) === decl) {
177
- // enum field is referenced
178
- return true;
83
+ writePolicy(writer, models, sourceFile) {
84
+ writer.write('policy:');
85
+ writer.inlineBlock(() => {
86
+ for (const model of models) {
87
+ writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model.name)}:`);
88
+ writer.block(() => {
89
+ // model-level guards
90
+ this.writeModelLevelDefs(model, writer, sourceFile);
91
+ // field-level guards
92
+ this.writeFieldLevelDefs(model, writer, sourceFile);
93
+ });
94
+ writer.writeLine(',');
179
95
  }
180
- return false;
181
96
  });
97
+ writer.writeLine(',');
182
98
  }
183
- getPolicyExpressions(target, kind, operation, override = false) {
184
- const attributes = target.attributes;
185
- const attrName = (0, ast_1.isDataModel)(target) ? `@@${kind}` : `@${kind}`;
186
- const attrs = attributes.filter((attr) => {
187
- var _a;
188
- if (((_a = attr.decl.ref) === null || _a === void 0 ? void 0 : _a.name) !== attrName) {
189
- return false;
190
- }
191
- if (override) {
192
- const overrideArg = (0, sdk_1.getAttributeArg)(attr, 'override');
193
- return overrideArg && (0, sdk_1.getLiteral)(overrideArg) === true;
194
- }
195
- else {
196
- return true;
197
- }
99
+ // #region Model-level definitions
100
+ // writes model-level policy def for each operation kind for a model
101
+ // `[modelName]: { [operationKind]: [funcName] },`
102
+ writeModelLevelDefs(model, writer, sourceFile) {
103
+ const policies = (0, sdk_1.analyzePolicies)(model);
104
+ writer.write('modelLevel:');
105
+ writer.inlineBlock(() => {
106
+ this.writeModelReadDef(model, policies, writer, sourceFile);
107
+ this.writeModelCreateDef(model, policies, writer, sourceFile);
108
+ this.writeModelUpdateDef(model, policies, writer, sourceFile);
109
+ this.writeModelPostUpdateDef(model, policies, writer, sourceFile);
110
+ this.writeModelDeleteDef(model, policies, writer, sourceFile);
198
111
  });
199
- const checkOperation = operation === 'postUpdate' ? 'update' : operation;
200
- let result = attrs
201
- .filter((attr) => {
202
- const opsValue = (0, sdk_1.getLiteral)(attr.args[0].value);
203
- if (!opsValue) {
204
- return false;
205
- }
206
- const ops = opsValue.split(',').map((s) => s.trim());
207
- return ops.includes(checkOperation) || ops.includes('all');
208
- })
209
- .map((attr) => attr.args[1].value);
210
- if (operation === 'update') {
211
- result = this.processUpdatePolicies(result, false);
212
- }
213
- else if (operation === 'postUpdate') {
214
- result = this.processUpdatePolicies(result, true);
215
- }
216
- return result;
112
+ writer.writeLine(',');
217
113
  }
218
- processUpdatePolicies(expressions, postUpdate) {
219
- const hasFutureReference = expressions.some((expr) => this.hasFutureReference(expr));
220
- if (postUpdate) {
221
- // when compiling post-update rules, if any rule contains `future()` reference,
222
- // we include all as post-update rules
223
- return hasFutureReference ? expressions : [];
224
- }
225
- else {
226
- // when compiling pre-update rules, if any rule contains `future()` reference,
227
- // we completely skip pre-update check and defer them to post-update
228
- return hasFutureReference ? [] : expressions;
229
- }
114
+ // writes `read: ...` for a given model
115
+ writeModelReadDef(model, policies, writer, sourceFile) {
116
+ writer.write(`read:`);
117
+ writer.inlineBlock(() => {
118
+ this.writeCommonModelDef(model, 'read', policies, writer, sourceFile);
119
+ });
120
+ writer.writeLine(',');
230
121
  }
231
- hasFutureReference(expr) {
232
- var _a;
233
- for (const node of (0, langium_1.streamAst)(expr)) {
234
- if ((0, ast_1.isInvocationExpr)(node) && ((_a = node.function.ref) === null || _a === void 0 ? void 0 : _a.name) === 'future' && (0, sdk_1.isFromStdlib)(node.function.ref)) {
235
- return true;
236
- }
122
+ // writes `create: ...` for a given model
123
+ writeModelCreateDef(model, policies, writer, sourceFile) {
124
+ writer.write(`create:`);
125
+ writer.inlineBlock(() => {
126
+ this.writeCommonModelDef(model, 'create', policies, writer, sourceFile);
127
+ // create policy has an additional input checker for validating the payload
128
+ this.writeCreateInputChecker(model, writer, sourceFile);
129
+ });
130
+ writer.writeLine(',');
131
+ }
132
+ // writes `inputChecker: [funcName]` for a given model
133
+ writeCreateInputChecker(model, writer, sourceFile) {
134
+ if (this.canCheckCreateBasedOnInput(model)) {
135
+ const inputCheckFunc = this.generateCreateInputCheckerFunction(model, sourceFile);
136
+ writer.write(`inputChecker: ${inputCheckFunc.getName()},`);
237
137
  }
238
- return false;
239
138
  }
240
- generateQueryGuardForModel(model, sourceFile) {
241
- return __awaiter(this, void 0, void 0, function* () {
242
- const result = {};
243
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
- const policies = (0, sdk_1.analyzePolicies)(model);
245
- for (const kind of plugin_utils_1.ALL_OPERATION_KINDS) {
246
- if (policies[kind] === true || policies[kind] === false) {
247
- result[kind] = policies[kind];
248
- if (kind === 'create') {
249
- result[kind + '_input'] = policies[kind];
250
- }
251
- continue;
139
+ canCheckCreateBasedOnInput(model) {
140
+ const allows = (0, utils_1.getPolicyExpressions)(model, 'allow', 'create', false, 'all');
141
+ const denies = (0, utils_1.getPolicyExpressions)(model, 'deny', 'create', false, 'all');
142
+ return [...allows, ...denies].every((rule) => {
143
+ return (0, langium_1.streamAst)(rule).every((expr) => {
144
+ var _a;
145
+ if ((0, ast_1.isThisExpr)(expr)) {
146
+ return false;
252
147
  }
253
- const denies = this.getPolicyExpressions(model, 'deny', kind);
254
- const allows = this.getPolicyExpressions(model, 'allow', kind);
255
- if (kind === 'update' && allows.length === 0) {
256
- // no allow rule for 'update', policy is constant based on if there's
257
- // post-update counterpart
258
- if (this.getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) {
259
- result[kind] = false;
260
- continue;
148
+ if ((0, ast_1.isReferenceExpr)(expr)) {
149
+ if ((0, ast_1.isDataModel)((_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) {
150
+ // if policy rules uses relation fields,
151
+ // we can't check based on create input
152
+ return false;
261
153
  }
262
- else {
263
- result[kind] = true;
264
- continue;
154
+ if ((0, ast_1.isDataModelField)(expr.target.ref) &&
155
+ expr.target.ref.$container === model &&
156
+ (0, sdk_1.hasAttribute)(expr.target.ref, '@default')) {
157
+ // reference to field of current model
158
+ // if it has default value, we can't check
159
+ // based on create input
160
+ return false;
265
161
  }
266
- }
267
- if (kind === 'postUpdate' && allows.length === 0 && denies.length === 0) {
268
- // no rule 'postUpdate', always allow
269
- result[kind] = true;
270
- continue;
271
- }
272
- const guardFunc = this.generateQueryGuardFunction(sourceFile, model, kind, allows, denies);
273
- result[kind] = guardFunc.getName();
274
- if (kind === 'postUpdate') {
275
- const preValueSelect = this.generateSelectForRules([...allows, ...denies]);
276
- if (preValueSelect) {
277
- result[runtime_1.PRE_UPDATE_VALUE_SELECTOR] = preValueSelect;
162
+ if ((0, ast_1.isDataModelField)(expr.target.ref) && (0, sdk_1.isForeignKeyField)(expr.target.ref)) {
163
+ // reference to foreign key field
164
+ // we can't check based on create input
165
+ return false;
278
166
  }
279
167
  }
280
- if (kind === 'create' && this.canCheckCreateBasedOnInput(model, allows, denies)) {
281
- const inputCheckFunc = this.generateInputCheckFunction(sourceFile, model, kind, allows, denies);
282
- result[kind + '_input'] = inputCheckFunc.getName();
283
- }
284
- }
285
- // generate field read checkers
286
- this.generateReadFieldsCheckers(model, sourceFile, result);
287
- // generate field read override guards
288
- this.generateReadFieldsOverrideGuards(model, sourceFile, result);
289
- // generate field update guards
290
- this.generateUpdateFieldsGuards(model, sourceFile, result);
291
- return result;
168
+ return true;
169
+ });
292
170
  });
293
171
  }
294
- generateReadFieldsCheckers(model, sourceFile, result) {
295
- const allFieldsAllows = [];
296
- const allFieldsDenies = [];
297
- for (const field of model.fields) {
298
- const allows = this.getPolicyExpressions(field, 'allow', 'read');
299
- const denies = this.getPolicyExpressions(field, 'deny', 'read');
300
- if (denies.length === 0 && allows.length === 0) {
301
- continue;
302
- }
303
- allFieldsAllows.push(...allows);
304
- allFieldsDenies.push(...denies);
305
- const guardFunc = this.generateReadFieldCheckerFunction(sourceFile, field, allows, denies);
306
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
307
- result[`${runtime_1.FIELD_LEVEL_READ_CHECKER_PREFIX}${field.name}`] = guardFunc.getName();
308
- }
309
- if (allFieldsAllows.length > 0 || allFieldsDenies.length > 0) {
310
- result[runtime_1.HAS_FIELD_LEVEL_POLICY_FLAG] = true;
311
- const readFieldCheckSelect = this.generateSelectForRules([...allFieldsAllows, ...allFieldsDenies]);
312
- if (readFieldCheckSelect) {
313
- result[runtime_1.FIELD_LEVEL_READ_CHECKER_SELECTOR] = readFieldCheckSelect;
314
- }
315
- }
316
- }
317
- generateReadFieldCheckerFunction(sourceFile, field, allows, denies) {
172
+ // generates a function for checking "create" input
173
+ generateCreateInputCheckerFunction(model, sourceFile) {
318
174
  const statements = [];
319
- this.generateNormalizedAuthRef(field.$container, allows, denies, statements);
320
- // compile rules down to typescript expressions
175
+ const allows = (0, utils_1.getPolicyExpressions)(model, 'allow', 'create');
176
+ const denies = (0, utils_1.getPolicyExpressions)(model, 'deny', 'create');
177
+ (0, utils_1.generateNormalizedAuthRef)(model, allows, denies, statements);
321
178
  statements.push((writer) => {
179
+ if (allows.length === 0) {
180
+ writer.write('return false;');
181
+ return;
182
+ }
322
183
  const transformer = new sdk_1.TypeScriptExpressionTransformer({
323
184
  context: sdk_1.ExpressionContext.AccessPolicy,
324
185
  fieldReferenceContext: 'input',
325
186
  });
326
- const denyStmt = denies.length > 0
187
+ let expr = denies.length > 0
327
188
  ? '!(' +
328
189
  denies
329
190
  .map((deny) => {
@@ -332,32 +193,16 @@ class PolicyGenerator {
332
193
  .join(' || ') +
333
194
  ')'
334
195
  : undefined;
335
- const allowStmt = allows.length > 0
336
- ? '(' +
337
- allows
338
- .map((allow) => {
339
- return transformer.transform(allow);
340
- })
341
- .join(' || ') +
342
- ')'
343
- : undefined;
344
- let expr;
345
- if (denyStmt && allowStmt) {
346
- expr = `${denyStmt} && ${allowStmt}`;
347
- }
348
- else if (denyStmt) {
349
- expr = denyStmt;
350
- }
351
- else if (allowStmt) {
352
- expr = allowStmt;
353
- }
354
- else {
355
- throw new Error('should not happen');
356
- }
196
+ const allowStmt = allows
197
+ .map((allow) => {
198
+ return transformer.transform(allow);
199
+ })
200
+ .join(' || ');
201
+ expr = expr ? `${expr} && (${allowStmt})` : allowStmt;
357
202
  writer.write('return ' + expr);
358
203
  });
359
204
  const func = sourceFile.addFunction({
360
- name: `${field.$container.name}$${field.name}_read`,
205
+ name: model.name + '_create_input',
361
206
  returnType: 'boolean',
362
207
  parameters: [
363
208
  {
@@ -373,294 +218,221 @@ class PolicyGenerator {
373
218
  });
374
219
  return func;
375
220
  }
376
- generateReadFieldsOverrideGuards(model, sourceFile, result) {
377
- for (const field of model.fields) {
378
- const overrideAllows = this.getPolicyExpressions(field, 'allow', 'read', true);
379
- if (overrideAllows.length > 0) {
380
- const denies = this.getPolicyExpressions(field, 'deny', 'read');
381
- const overrideGuardFunc = this.generateQueryGuardFunction(sourceFile, model, 'read', overrideAllows, denies, field, true);
382
- result[`${runtime_1.FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX}${field.name}`] = overrideGuardFunc.getName();
383
- }
384
- }
221
+ // writes `update: ...` for a given model
222
+ writeModelUpdateDef(model, policies, writer, sourceFile) {
223
+ writer.write(`update:`);
224
+ writer.inlineBlock(() => {
225
+ this.writeCommonModelDef(model, 'update', policies, writer, sourceFile);
226
+ });
227
+ writer.writeLine(',');
385
228
  }
386
- generateUpdateFieldsGuards(model, sourceFile, result) {
387
- for (const field of model.fields) {
388
- const allows = this.getPolicyExpressions(field, 'allow', 'update');
389
- const denies = this.getPolicyExpressions(field, 'deny', 'update');
390
- if (denies.length === 0 && allows.length === 0) {
391
- continue;
392
- }
393
- const guardFunc = this.generateQueryGuardFunction(sourceFile, model, 'update', allows, denies, field);
394
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
395
- result[`${runtime_1.FIELD_LEVEL_UPDATE_GUARD_PREFIX}${field.name}`] = guardFunc.getName();
396
- const overrideAllows = this.getPolicyExpressions(field, 'allow', 'update', true);
397
- if (overrideAllows.length > 0) {
398
- const overrideGuardFunc = this.generateQueryGuardFunction(sourceFile, model, 'update', overrideAllows, denies, field, true);
399
- result[`${runtime_1.FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX}${field.name}`] = overrideGuardFunc.getName();
400
- }
229
+ // writes `postUpdate: ...` for a given model
230
+ writeModelPostUpdateDef(model, policies, writer, sourceFile) {
231
+ writer.write(`postUpdate:`);
232
+ writer.inlineBlock(() => {
233
+ this.writeCommonModelDef(model, 'postUpdate', policies, writer, sourceFile);
234
+ // post-update policy has an additional selector for reading the pre-update entity data
235
+ this.writePostUpdatePreValueSelector(model, writer);
236
+ });
237
+ writer.writeLine(',');
238
+ }
239
+ writePostUpdatePreValueSelector(model, writer) {
240
+ const allows = (0, utils_1.getPolicyExpressions)(model, 'allow', 'postUpdate');
241
+ const denies = (0, utils_1.getPolicyExpressions)(model, 'deny', 'postUpdate');
242
+ const preValueSelect = (0, utils_1.generateSelectForRules)([...allows, ...denies]);
243
+ if (preValueSelect) {
244
+ writer.writeLine(`preUpdateSelector: ${JSON.stringify(preValueSelect)},`);
401
245
  }
402
246
  }
403
- canCheckCreateBasedOnInput(model, allows, denies) {
404
- return [...allows, ...denies].every((rule) => {
405
- return (0, langium_1.streamAst)(rule).every((expr) => {
406
- var _a;
407
- if ((0, ast_1.isThisExpr)(expr)) {
408
- return false;
409
- }
410
- if ((0, ast_1.isReferenceExpr)(expr)) {
411
- if ((0, ast_1.isDataModel)((_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) {
412
- // if policy rules uses relation fields,
413
- // we can't check based on create input
414
- return false;
415
- }
416
- if ((0, ast_1.isDataModelField)(expr.target.ref) &&
417
- expr.target.ref.$container === model &&
418
- (0, sdk_1.hasAttribute)(expr.target.ref, '@default')) {
419
- // reference to field of current model
420
- // if it has default value, we can't check
421
- // based on create input
422
- return false;
423
- }
424
- if ((0, ast_1.isDataModelField)(expr.target.ref) && (0, sdk_1.isForeignKeyField)(expr.target.ref)) {
425
- // reference to foreign key field
426
- // we can't check based on create input
427
- return false;
428
- }
429
- }
430
- return true;
431
- });
247
+ // writes `delete: ...` for a given model
248
+ writeModelDeleteDef(model, policies, writer, sourceFile) {
249
+ writer.write(`delete:`);
250
+ writer.inlineBlock(() => {
251
+ this.writeCommonModelDef(model, 'delete', policies, writer, sourceFile);
432
252
  });
433
253
  }
434
- // generates a "select" object that contains (recursively) fields referenced by the
435
- // given policy rules
436
- generateSelectForRules(rules, forAuthContext = false) {
437
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
438
- const result = {};
439
- const addPath = (path) => {
440
- let curr = result;
441
- path.forEach((seg, i) => {
442
- if (i === path.length - 1) {
443
- curr[seg] = true;
444
- }
445
- else {
446
- if (!curr[seg]) {
447
- curr[seg] = { select: {} };
448
- }
449
- curr = curr[seg].select;
450
- }
451
- });
452
- };
453
- // visit a reference or member access expression to build a
454
- // selection path
455
- const visit = (node) => {
456
- if ((0, ast_1.isThisExpr)(node)) {
457
- return [];
458
- }
459
- if ((0, ast_1.isReferenceExpr)(node)) {
460
- const target = (0, sdk_1.resolved)(node.target);
461
- if ((0, ast_1.isDataModelField)(target)) {
462
- // a field selection, it's a terminal
463
- return [target.name];
464
- }
465
- }
466
- if ((0, ast_1.isMemberAccessExpr)(node)) {
467
- if (forAuthContext && (0, sdk_1.isAuthInvocation)(node.operand)) {
468
- return [node.member.$refText];
469
- }
470
- if ((0, sdk_1.isFutureExpr)(node.operand)) {
471
- // future().field is not subject to pre-update select
472
- return undefined;
473
- }
474
- // build a selection path inside-out for chained member access
475
- const inner = visit(node.operand);
476
- if (inner) {
477
- return [...inner, node.member.$refText];
478
- }
479
- }
480
- return undefined;
481
- };
482
- // collect selection paths from the given expression
483
- const collectReferencePaths = (expr) => {
484
- var _a, _b, _c;
485
- if ((0, ast_1.isThisExpr)(expr) && !(0, ast_1.isMemberAccessExpr)(expr.$container)) {
486
- // a standalone `this` expression, include all id fields
487
- const model = (_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl;
488
- const idFields = (0, sdk_1.getIdFields)(model);
489
- return idFields.map((field) => [field.name]);
490
- }
491
- if ((0, ast_1.isMemberAccessExpr)(expr) || (0, ast_1.isReferenceExpr)(expr)) {
492
- const path = visit(expr);
493
- if (path) {
494
- if ((0, ast_1.isDataModel)((_b = expr.$resolvedType) === null || _b === void 0 ? void 0 : _b.decl)) {
495
- // member selection ended at a data model field, include its id fields
496
- const idFields = (0, sdk_1.getIdFields)((_c = expr.$resolvedType) === null || _c === void 0 ? void 0 : _c.decl);
497
- return idFields.map((field) => [...path, field.name]);
498
- }
499
- else {
500
- return [path];
501
- }
502
- }
503
- else {
504
- return [];
505
- }
254
+ // writes `[kind]: ...` for a given model
255
+ writeCommonModelDef(model, kind, policies, writer, sourceFile) {
256
+ const allows = (0, utils_1.getPolicyExpressions)(model, 'allow', kind);
257
+ const denies = (0, utils_1.getPolicyExpressions)(model, 'deny', kind);
258
+ // policy guard
259
+ this.writePolicyGuard(model, kind, policies, allows, denies, writer, sourceFile);
260
+ // permission checker
261
+ if (kind !== 'postUpdate') {
262
+ this.writePermissionChecker(model, kind, policies, allows, denies, writer, sourceFile);
263
+ }
264
+ // write cross-model comparison rules as entity checker functions
265
+ // because they cannot be checked inside Prisma
266
+ this.writeEntityChecker(model, kind, writer, sourceFile, true);
267
+ }
268
+ writeEntityChecker(target, kind, writer, sourceFile, onlyCrossModelComparison = false, forOverride = false) {
269
+ var _a;
270
+ const allows = (0, utils_1.getPolicyExpressions)(target, 'allow', kind, forOverride, onlyCrossModelComparison ? 'onlyCrossModelComparison' : 'all');
271
+ const denies = (0, utils_1.getPolicyExpressions)(target, 'deny', kind, forOverride, onlyCrossModelComparison ? 'onlyCrossModelComparison' : 'all');
272
+ if (allows.length === 0 && denies.length === 0) {
273
+ return;
274
+ }
275
+ const model = (0, ast_1.isDataModel)(target) ? target : target.$container;
276
+ const func = (0, utils_1.generateEntityCheckerFunction)(sourceFile, model, kind, allows, denies, (0, ast_1.isDataModelField)(target) ? target : undefined, forOverride);
277
+ const selector = (_a = (0, utils_1.generateSelectForRules)([...allows, ...denies], false, kind !== 'postUpdate')) !== null && _a !== void 0 ? _a : {};
278
+ const key = forOverride ? 'overrideEntityChecker' : 'entityChecker';
279
+ writer.write(`${key}: { func: ${func.getName()}, selector: ${JSON.stringify(selector)} },`);
280
+ }
281
+ // writes `guard: ...` for a given policy operation kind
282
+ writePolicyGuard(model, kind, policies, allows, denies, writer, sourceFile) {
283
+ if (kind === 'update' && allows.length === 0) {
284
+ // no allow rule for 'update', policy is constant based on if there's
285
+ // post-update counterpart
286
+ if ((0, utils_1.getPolicyExpressions)(model, 'allow', 'postUpdate').length === 0) {
287
+ writer.write(`guard: false,`);
506
288
  }
507
- else if ((0, ast_utils_1.isCollectionPredicate)(expr)) {
508
- const path = visit(expr.left);
509
- if (path) {
510
- // recurse into RHS
511
- const rhs = collectReferencePaths(expr.right);
512
- // combine path of LHS and RHS
513
- return rhs.map((r) => [...path, ...r]);
514
- }
515
- else {
516
- return [];
517
- }
289
+ else {
290
+ writer.write(`guard: true,`);
518
291
  }
519
- else if ((0, ast_1.isInvocationExpr)(expr)) {
520
- // recurse into function arguments
521
- return expr.args.flatMap((arg) => collectReferencePaths(arg.value));
292
+ return;
293
+ }
294
+ if (kind === 'postUpdate' && allows.length === 0 && denies.length === 0) {
295
+ // no 'postUpdate' rule, always allow
296
+ writer.write(`guard: true,`);
297
+ return;
298
+ }
299
+ if (kind in policies && typeof policies[kind] === 'boolean') {
300
+ // constant policy
301
+ writer.write(`guard: ${policies[kind]},`);
302
+ return;
303
+ }
304
+ // generate a policy function that evaluates a partial prisma query
305
+ const guardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, kind, allows, denies);
306
+ writer.write(`guard: ${guardFunc.getName()},`);
307
+ }
308
+ // writes `permissionChecker: ...` for a given policy operation kind
309
+ writePermissionChecker(model, kind, policies, allows, denies, writer, sourceFile) {
310
+ if (this.options.generatePermissionChecker !== true) {
311
+ return;
312
+ }
313
+ if (policies[kind] === true || policies[kind] === false) {
314
+ // constant policy
315
+ writer.write(`permissionChecker: ${policies[kind]},`);
316
+ return;
317
+ }
318
+ if (kind === 'update' && allows.length === 0) {
319
+ // no allow rule for 'update', policy is constant based on if there's
320
+ // post-update counterpart
321
+ if ((0, utils_1.getPolicyExpressions)(model, 'allow', 'postUpdate').length === 0) {
322
+ writer.write(`permissionChecker: false,`);
522
323
  }
523
324
  else {
524
- // recurse
525
- const children = (0, langium_1.streamContents)(expr)
526
- .filter((child) => (0, ast_1.isExpression)(child))
527
- .toArray();
528
- return children.flatMap((child) => collectReferencePaths(child));
325
+ writer.write(`permissionChecker: true,`);
529
326
  }
530
- };
531
- for (const rule of rules) {
532
- const paths = collectReferencePaths(rule);
533
- paths.forEach((p) => addPath(p));
327
+ return;
534
328
  }
535
- return Object.keys(result).length === 0 ? undefined : result;
329
+ const guardFunc = this.generatePermissionCheckerFunction(model, kind, allows, denies, sourceFile);
330
+ writer.write(`permissionChecker: ${guardFunc.getName()},`);
536
331
  }
537
- generateQueryGuardFunction(sourceFile, model, kind, allows, denies, forField, fieldOverride = false) {
332
+ generatePermissionCheckerFunction(model, kind, allows, denies, sourceFile) {
538
333
  const statements = [];
539
- this.generateNormalizedAuthRef(model, allows, denies, statements);
540
- const hasFieldAccess = [...denies, ...allows].some((rule) => (0, langium_1.streamAst)(rule).some((child) =>
541
- // this.???
542
- (0, ast_1.isThisExpr)(child) ||
543
- // future().???
544
- (0, sdk_1.isFutureExpr)(child) ||
545
- // field reference
546
- ((0, ast_1.isReferenceExpr)(child) && (0, ast_1.isDataModelField)(child.target.ref))));
547
- if (!hasFieldAccess) {
548
- // none of the rules reference model fields, we can compile down to a plain boolean
549
- // function in this case (so we can skip doing SQL queries when validating)
550
- statements.push((writer) => {
551
- const transformer = new sdk_1.TypeScriptExpressionTransformer({
552
- context: sdk_1.ExpressionContext.AccessPolicy,
553
- isPostGuard: kind === 'postUpdate',
554
- });
555
- try {
556
- denies.forEach((rule) => {
557
- writer.write(`if (${transformer.transform(rule, false)}) { return ${expression_writer_1.FALSE}; }`);
558
- });
559
- allows.forEach((rule) => {
560
- writer.write(`if (${transformer.transform(rule, false)}) { return ${expression_writer_1.TRUE}; }`);
561
- });
562
- }
563
- catch (err) {
564
- if (err instanceof sdk_1.TypeScriptExpressionTransformerError) {
565
- throw new sdk_1.PluginError(__1.name, err.message);
566
- }
567
- else {
568
- throw err;
569
- }
570
- }
571
- if (forField) {
572
- if (allows.length === 0) {
573
- // if there's no allow rule, for field-level rules, by default we allow
574
- writer.write(`return ${expression_writer_1.TRUE};`);
575
- }
576
- else {
577
- // if there's any allow rule, we deny unless any allow rule evaluates to true
578
- writer.write(`return ${expression_writer_1.FALSE};`);
579
- }
580
- }
581
- else {
582
- // for model-level rules, the default is always deny
583
- writer.write(`return ${expression_writer_1.FALSE};`);
584
- }
585
- });
586
- }
587
- else {
588
- statements.push((writer) => {
589
- writer.write('return ');
590
- const exprWriter = new expression_writer_1.ExpressionWriter(writer, kind === 'postUpdate');
591
- const writeDenies = () => {
592
- writer.conditionalWrite(denies.length > 1, '{ AND: [');
593
- denies.forEach((expr, i) => {
594
- writer.inlineBlock(() => {
595
- writer.write('NOT: ');
596
- exprWriter.write(expr);
597
- });
598
- writer.conditionalWrite(i !== denies.length - 1, ',');
599
- });
600
- writer.conditionalWrite(denies.length > 1, ']}');
601
- };
602
- const writeAllows = () => {
603
- writer.conditionalWrite(allows.length > 1, '{ OR: [');
604
- allows.forEach((expr, i) => {
605
- exprWriter.write(expr);
606
- writer.conditionalWrite(i !== allows.length - 1, ',');
607
- });
608
- writer.conditionalWrite(allows.length > 1, ']}');
609
- };
610
- if (allows.length > 0 && denies.length > 0) {
611
- // include both allow and deny rules
612
- writer.write('{ AND: [');
613
- writeDenies();
614
- writer.write(',');
615
- writeAllows();
616
- writer.write(']}');
617
- }
618
- else if (denies.length > 0) {
619
- // only deny rules
620
- writeDenies();
621
- }
622
- else if (allows.length > 0) {
623
- // only allow rules
624
- writeAllows();
625
- }
626
- else {
627
- // disallow any operation
628
- writer.write(`{ OR: [] }`);
629
- }
630
- writer.write(';');
631
- });
632
- }
334
+ (0, utils_1.generateNormalizedAuthRef)(model, allows, denies, statements);
335
+ const transformed = new constraint_transformer_1.ConstraintTransformer({
336
+ authAccessor: 'user',
337
+ }).transformRules(allows, denies);
338
+ statements.push(`return ${transformed};`);
633
339
  const func = sourceFile.addFunction({
634
- name: `${model.name}${forField ? '$' + forField.name : ''}${fieldOverride ? '$override' : ''}_${kind}`,
635
- returnType: 'any',
340
+ name: `${model.name}$checker$${kind}`,
341
+ returnType: 'PermissionCheckerConstraint',
636
342
  parameters: [
637
343
  {
638
344
  name: 'context',
639
- type: 'QueryContext',
640
- },
641
- {
642
- // for generating field references used by field comparison in the same model
643
- name: 'db',
644
- type: 'CrudContract',
345
+ type: 'PermissionCheckerContext',
645
346
  },
646
347
  ],
647
348
  statements,
648
349
  });
649
350
  return func;
650
351
  }
651
- generateInputCheckFunction(sourceFile, model, kind, allows, denies) {
352
+ // #endregion
353
+ // #region Field-level definitions
354
+ writeFieldLevelDefs(model, writer, sf) {
355
+ writer.write('fieldLevel:');
356
+ writer.inlineBlock(() => {
357
+ this.writeFieldReadDef(model, writer, sf);
358
+ this.writeFieldUpdateDef(model, writer, sf);
359
+ });
360
+ writer.writeLine(',');
361
+ }
362
+ writeFieldReadDef(model, writer, sourceFile) {
363
+ writer.writeLine('read:');
364
+ writer.block(() => {
365
+ for (const field of model.fields) {
366
+ const allows = (0, utils_1.getPolicyExpressions)(field, 'allow', 'read');
367
+ const denies = (0, utils_1.getPolicyExpressions)(field, 'deny', 'read');
368
+ const overrideAllows = (0, utils_1.getPolicyExpressions)(field, 'allow', 'read', true);
369
+ if (allows.length === 0 && denies.length === 0 && overrideAllows.length === 0) {
370
+ continue;
371
+ }
372
+ writer.write(`${field.name}:`);
373
+ writer.block(() => {
374
+ // guard
375
+ const guardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, 'read', allows, denies, field);
376
+ writer.write(`guard: ${guardFunc.getName()},`);
377
+ // checker function
378
+ // write all field-level rules as entity checker function
379
+ this.writeEntityChecker(field, 'read', writer, sourceFile, false, false);
380
+ if (overrideAllows.length > 0) {
381
+ // override guard function
382
+ const denies = (0, utils_1.getPolicyExpressions)(field, 'deny', 'read');
383
+ const overrideGuardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, 'read', overrideAllows, denies, field, true);
384
+ writer.write(`overrideGuard: ${overrideGuardFunc.getName()},`);
385
+ // additional entity checker for override
386
+ this.writeEntityChecker(field, 'read', writer, sourceFile, false, true);
387
+ }
388
+ });
389
+ writer.writeLine(',');
390
+ }
391
+ });
392
+ writer.writeLine(',');
393
+ }
394
+ writeFieldUpdateDef(model, writer, sourceFile) {
395
+ writer.writeLine('update:');
396
+ writer.block(() => {
397
+ for (const field of model.fields) {
398
+ const allows = (0, utils_1.getPolicyExpressions)(field, 'allow', 'update');
399
+ const denies = (0, utils_1.getPolicyExpressions)(field, 'deny', 'update');
400
+ const overrideAllows = (0, utils_1.getPolicyExpressions)(field, 'allow', 'update', true);
401
+ if (allows.length === 0 && denies.length === 0 && overrideAllows.length === 0) {
402
+ continue;
403
+ }
404
+ writer.write(`${field.name}:`);
405
+ writer.block(() => {
406
+ // guard
407
+ const guardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, 'update', allows, denies, field);
408
+ writer.write(`guard: ${guardFunc.getName()},`);
409
+ // write cross-model comparison rules as entity checker functions
410
+ // because they cannot be checked inside Prisma
411
+ this.writeEntityChecker(field, 'update', writer, sourceFile, true, false);
412
+ if (overrideAllows.length > 0) {
413
+ // override guard
414
+ const overrideGuardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, 'update', overrideAllows, denies, field, true);
415
+ writer.write(`overrideGuard: ${overrideGuardFunc.getName()},`);
416
+ // write cross-model comparison override rules as entity checker functions
417
+ // because they cannot be checked inside Prisma
418
+ this.writeEntityChecker(field, 'update', writer, sourceFile, true, true);
419
+ }
420
+ });
421
+ writer.writeLine(',');
422
+ }
423
+ });
424
+ writer.writeLine(',');
425
+ }
426
+ generateFieldReadCheckerFunction(sourceFile, field, allows, denies) {
652
427
  const statements = [];
653
- this.generateNormalizedAuthRef(model, allows, denies, statements);
428
+ (0, utils_1.generateNormalizedAuthRef)(field.$container, allows, denies, statements);
429
+ // compile rules down to typescript expressions
654
430
  statements.push((writer) => {
655
- if (allows.length === 0) {
656
- writer.write('return false;');
657
- return;
658
- }
659
431
  const transformer = new sdk_1.TypeScriptExpressionTransformer({
660
432
  context: sdk_1.ExpressionContext.AccessPolicy,
661
433
  fieldReferenceContext: 'input',
662
434
  });
663
- let expr = denies.length > 0
435
+ const denyStmt = denies.length > 0
664
436
  ? '!(' +
665
437
  denies
666
438
  .map((deny) => {
@@ -669,16 +441,32 @@ class PolicyGenerator {
669
441
  .join(' || ') +
670
442
  ')'
671
443
  : undefined;
672
- const allowStmt = allows
673
- .map((allow) => {
674
- return transformer.transform(allow);
675
- })
676
- .join(' || ');
677
- expr = expr ? `${expr} && (${allowStmt})` : allowStmt;
444
+ const allowStmt = allows.length > 0
445
+ ? '(' +
446
+ allows
447
+ .map((allow) => {
448
+ return transformer.transform(allow);
449
+ })
450
+ .join(' || ') +
451
+ ')'
452
+ : undefined;
453
+ let expr;
454
+ if (denyStmt && allowStmt) {
455
+ expr = `${denyStmt} && ${allowStmt}`;
456
+ }
457
+ else if (denyStmt) {
458
+ expr = denyStmt;
459
+ }
460
+ else if (allowStmt) {
461
+ expr = allowStmt;
462
+ }
463
+ else {
464
+ throw new Error('should not happen');
465
+ }
678
466
  writer.write('return ' + expr);
679
467
  });
680
468
  const func = sourceFile.addFunction({
681
- name: model.name + '_' + kind + '_input',
469
+ name: `${field.$container.name}$${field.name}_read`,
682
470
  returnType: 'boolean',
683
471
  parameters: [
684
472
  {
@@ -694,71 +482,58 @@ class PolicyGenerator {
694
482
  });
695
483
  return func;
696
484
  }
697
- generateNormalizedAuthRef(model, allows, denies, statements) {
698
- // check if any allow or deny rule contains 'auth()' invocation
699
- const hasAuthRef = [...allows, ...denies].some((rule) => (0, langium_1.streamAst)(rule).some((child) => (0, sdk_1.isAuthInvocation)(child)));
700
- if (hasAuthRef) {
701
- const authModel = (0, sdk_1.getAuthModel)((0, sdk_1.getDataModels)(model.$container, true));
702
- if (!authModel) {
703
- throw new sdk_1.PluginError(__1.name, 'Auth model not found');
704
- }
705
- const userIdFields = (0, sdk_1.getIdFields)(authModel);
706
- if (!userIdFields || userIdFields.length === 0) {
707
- throw new sdk_1.PluginError(__1.name, 'User model does not have an id field');
708
- }
709
- // normalize user to null to avoid accidentally use undefined in filter
710
- statements.push(`const user: any = context.user ?? null;`);
485
+ // #endregion
486
+ //#region Auth selector
487
+ writeAuthSelector(models, writer) {
488
+ const authSelector = this.generateAuthSelector(models);
489
+ if (authSelector) {
490
+ writer.write(`authSelector: ${JSON.stringify(authSelector)},`);
711
491
  }
712
492
  }
713
- generateCheckerForModel(model, sourceFile) {
714
- return __awaiter(this, void 0, void 0, function* () {
715
- const result = {};
716
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
717
- const policies = (0, sdk_1.analyzePolicies)(model);
718
- for (const kind of ['create', 'read', 'update', 'delete']) {
719
- if (policies[kind] === true || policies[kind] === false) {
720
- result[kind] = policies[kind];
721
- continue;
722
- }
723
- const denies = this.getPolicyExpressions(model, 'deny', kind);
724
- const allows = this.getPolicyExpressions(model, 'allow', kind);
725
- if (kind === 'update' && allows.length === 0) {
726
- // no allow rule for 'update', policy is constant based on if there's
727
- // post-update counterpart
728
- if (this.getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) {
729
- result[kind] = false;
730
- continue;
731
- }
732
- else {
733
- result[kind] = true;
734
- continue;
493
+ // Generates a { select: ... } object to select `auth()` fields used in policy rules
494
+ generateAuthSelector(models) {
495
+ const authRules = [];
496
+ models.forEach((model) => {
497
+ // model-level rules
498
+ const modelPolicyAttrs = model.attributes.filter((attr) => ['@@allow', '@@deny'].includes(attr.decl.$refText));
499
+ // field-level rules
500
+ const fieldPolicyAttrs = model.fields
501
+ .flatMap((f) => f.attributes)
502
+ .filter((attr) => ['@allow', '@deny'].includes(attr.decl.$refText));
503
+ // all rule expression
504
+ const allExpressions = [...modelPolicyAttrs, ...fieldPolicyAttrs]
505
+ .filter((attr) => attr.args.length > 1)
506
+ .map((attr) => attr.args[1].value);
507
+ // collect `auth()` member access
508
+ allExpressions.forEach((rule) => {
509
+ (0, langium_1.streamAst)(rule).forEach((node) => {
510
+ if ((0, ast_1.isMemberAccessExpr)(node) && (0, sdk_1.isAuthInvocation)(node.operand)) {
511
+ authRules.push(node);
735
512
  }
736
- }
737
- const guardFunc = this.generateCheckerFunction(sourceFile, model, kind, allows, denies);
738
- result[kind] = guardFunc.getName();
739
- }
740
- return result;
513
+ });
514
+ });
741
515
  });
516
+ if (authRules.length > 0) {
517
+ return (0, utils_1.generateSelectForRules)(authRules, true);
518
+ }
519
+ else {
520
+ return undefined;
521
+ }
742
522
  }
743
- generateCheckerFunction(sourceFile, model, kind, allows, denies) {
744
- const statements = [];
745
- this.generateNormalizedAuthRef(model, allows, denies, statements);
746
- const transformed = new constraint_transformer_1.ConstraintTransformer({
747
- authAccessor: 'user',
748
- }).transformRules(allows, denies);
749
- statements.push(`return ${transformed};`);
750
- const func = sourceFile.addFunction({
751
- name: `${model.name}$checker$${kind}`,
752
- returnType: 'CheckerConstraint',
753
- parameters: [
754
- {
755
- name: 'context',
756
- type: 'CheckerContext',
757
- },
758
- ],
759
- statements,
523
+ // #endregion
524
+ // #region Validation meta
525
+ writeValidationMeta(writer, models) {
526
+ writer.write('validation:');
527
+ writer.inlineBlock(() => {
528
+ for (const model of models) {
529
+ writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model.name)}:`);
530
+ writer.inlineBlock(() => {
531
+ writer.write(`hasValidation: ${(0, sdk_1.hasValidationAttributes)(model)}`);
532
+ });
533
+ writer.writeLine(',');
534
+ }
760
535
  });
761
- return func;
536
+ writer.writeLine(',');
762
537
  }
763
538
  }
764
539
  exports.PolicyGenerator = PolicyGenerator;