From e415370dda8c779799796f2547840daa4f8475ab Mon Sep 17 00:00:00 2001 From: Yiming <104139426+ymc9@users.noreply.github.com> Date: Sat, 4 Feb 2023 21:09:30 +0800 Subject: [PATCH] merge dev to main (#197) --- package.json | 2 +- packages/language/package.json | 2 +- packages/language/src/generated/grammar.ts | 127 +++++++++++- packages/language/src/zmodel.langium | 26 +-- packages/next/package.json | 2 +- packages/plugins/react/package.json | 2 +- .../react/src/react-hooks-generator.ts | 17 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- .../src/enhancements/policy/policy-utils.ts | 47 ++++- packages/runtime/src/enhancements/types.ts | 11 +- packages/schema/package.json | 2 +- .../schema/src/plugins/model-meta/index.ts | 48 ++++- packages/sdk/package.json | 2 +- packages/sdk/src/utils.ts | 21 +- tests/integration/test-run/package-lock.json | 4 +- .../with-policy/multi-field-unique.test.ts | 193 ++++++++++++++++++ 17 files changed, 460 insertions(+), 50 deletions(-) create mode 100644 tests/integration/tests/with-policy/multi-field-unique.test.ts diff --git a/package.json b/package.json index 2a6d99669..b8864a499 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.31", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index b499fd6a1..94dcb52da 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.31", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index d82b57737..989c7f1da 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -104,6 +104,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "datasource" @@ -156,6 +164,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "name", @@ -211,6 +227,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "generator" @@ -263,6 +287,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "name", @@ -318,6 +350,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "plugin" @@ -370,6 +410,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "name", @@ -1589,6 +1637,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "enum" @@ -1639,16 +1695,29 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "ParserRule", "name": "EnumField", "definition": { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@53" + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" }, - "arguments": [] - } + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@53" + }, + "arguments": [] + } + } + ] }, "definesHiddenTokens": false, "entry": false, @@ -1663,6 +1732,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "function" @@ -1779,6 +1856,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "name", @@ -2034,6 +2119,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Keyword", "value": "attribute" @@ -2126,6 +2219,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "default", @@ -2323,6 +2424,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "Group", "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [], + "cardinality": "*" + }, { "$type": "Assignment", "feature": "decl", diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 681608f3c..d6feb0179 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -10,24 +10,24 @@ AbstractDeclaration: // datasource DataSource: - 'datasource' name=ID '{' (fields+=DataSourceField)* '}'; + TRIPLE_SLASH_COMMENT* 'datasource' name=ID '{' (fields+=DataSourceField)* '}'; DataSourceField: - name=ID '=' value=(LiteralExpr|InvocationExpr); + TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr|InvocationExpr); // generator GeneratorDecl: - 'generator' name=ID '{' (fields+=GeneratorField)* '}'; + TRIPLE_SLASH_COMMENT* 'generator' name=ID '{' (fields+=GeneratorField)* '}'; GeneratorField: - name=ID '=' value=(LiteralExpr | ArrayExpr); + TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr | ArrayExpr); // plugin Plugin: - 'plugin' name=ID '{' (fields+=PluginField)* '}'; + TRIPLE_SLASH_COMMENT* 'plugin' name=ID '{' (fields+=PluginField)* '}'; PluginField: - name=ID '=' value=(LiteralExpr | ArrayExpr); + TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr | ArrayExpr); // expression Expression: @@ -149,17 +149,17 @@ DataModelFieldType: // enum Enum: - 'enum' name=ID '{' (fields+=EnumField)+ '}'; + TRIPLE_SLASH_COMMENT* 'enum' name=ID '{' (fields+=EnumField)+ '}'; EnumField: - name=ID; + TRIPLE_SLASH_COMMENT* name=ID; // function FunctionDecl: - 'function' name=ID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}'; + TRIPLE_SLASH_COMMENT* 'function' name=ID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}'; FunctionParam: - name=ID ':' type=FunctionParamType; + TRIPLE_SLASH_COMMENT* name=ID ':' type=FunctionParamType; FunctionParamType: (type=ExpressionType | reference=[TypeDeclaration]) (array?='[]')?; @@ -184,10 +184,10 @@ AttributeName returns string: // attribute Attribute: - 'attribute' name=AttributeName '(' (params+=AttributeParam (',' params+=AttributeParam)*)? ')' (attributes+=AttributeAttribute)*; + TRIPLE_SLASH_COMMENT* 'attribute' name=AttributeName '(' (params+=AttributeParam (',' params+=AttributeParam)*)? ')' (attributes+=AttributeAttribute)*; AttributeParam: - (default?='_')? name=ID ':' type=AttributeParamType; + TRIPLE_SLASH_COMMENT* (default?='_')? name=ID ':' type=AttributeParamType; // FieldReference refers to fields declared in the current model // TransitiveFieldReference refers to fields declared in the model type of the current field @@ -200,7 +200,7 @@ DataModelFieldAttribute: decl=[Attribute:DataModelFieldAttributeName] ('(' AttributeArgList? ')')?; DataModelAttribute: - decl=[Attribute:DataModelAttributeName] ('(' AttributeArgList? ')')?; + TRIPLE_SLASH_COMMENT* decl=[Attribute:DataModelAttributeName] ('(' AttributeArgList? ')')?; AttributeAttribute: decl=[Attribute:AttributeAttributeName] ('(' AttributeArgList? ')')?; diff --git a/packages/next/package.json b/packages/next/package.json index 0351fda53..334de8570 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.31", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index da98367b8..bac8594ee 100644 --- a/packages/plugins/react/package.json +++ b/packages/plugins/react/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/react", "displayName": "ZenStack plugin and runtime for ReactJS", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.31", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/react/src/react-hooks-generator.ts b/packages/plugins/react/src/react-hooks-generator.ts index d34f97a47..58b9a33f9 100644 --- a/packages/plugins/react/src/react-hooks-generator.ts +++ b/packages/plugins/react/src/react-hooks-generator.ts @@ -6,17 +6,6 @@ import * as path from 'path'; import { Project } from 'ts-morph'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - const project = new Project(); - const models: DataModel[] = []; - const warnings: string[] = []; - - for (const dm of model.declarations.filter((d): d is DataModel => isDataModel(d))) { - const hasAllowRule = dm.attributes.find((attr) => attr.decl.ref?.name === '@@allow'); - if (hasAllowRule) { - models.push(dm); - } - } - let outDir = options.output as string; if (!outDir) { throw new PluginError('"output" option is required'); @@ -27,6 +16,10 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. outDir = path.join(path.dirname(options.schemaPath), outDir); } + const project = new Project(); + const warnings: string[] = []; + const models = model.declarations.filter((d): d is DataModel => isDataModel(d)); + generateIndex(project, outDir, models); models.forEach((model) => { @@ -42,7 +35,7 @@ function wrapReadbackErrorCheck(code: string) { return `try { ${code} } catch (err: any) { - if (err.prisma && err.code === 'P2004') { + if (err.info?.prisma && err.info?.code === 'P2004') { // unable to readback data return undefined; } else { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 65f6ed6e5..3ef3b3492 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.31", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 2fcaca000..b53331462 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.31", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 4afee5791..7ca356aba 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -130,6 +130,14 @@ export class PolicyUtil { */ async readWithCheck(model: string, args: any): Promise { args = this.clone(args); + + if (args.where) { + // query args will be used with findMany, so we need to + // translate unique constraint filters into a flat filter + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + await this.flattenGeneratedUniqueField(model, args.where); + } + await this.injectAuthGuard(args, model, 'read'); // recursively inject read guard conditions into the query args @@ -143,6 +151,28 @@ export class PolicyUtil { return result; } + // flatten unique constraint filters + async flattenGeneratedUniqueField(model: string, args: any) { + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + const uniqueConstraints = this.modelMeta.uniqueConstraints?.[camelCase(model)]; + let flattened = false; + if (uniqueConstraints) { + for (const [field, value] of Object.entries(args)) { + if (uniqueConstraints[field] && typeof value === 'object') { + for (const [f, v] of Object.entries(value)) { + args[f] = v; + } + delete args[field]; + flattened = true; + } + } + } + + if (flattened) { + this.logger.info(`Filter flattened: ${JSON.stringify(args)}`); + } + } + private async injectNestedReadConditions(model: string, args: any) { const injectTarget = args.select ?? args.include; if (!injectTarget) { @@ -376,6 +406,12 @@ export class PolicyUtil { // fetch preValue selection (analyzed from the post-update rules) const preValueSelect = await this.getPreValueSelect(model); const filter = await buildReversedQuery(context); + + // query args will be used with findMany, so we need to + // translate unique constraint filters into a flat filter + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + await this.flattenGeneratedUniqueField(model, filter); + const idField = this.getIdField(model); const query = { where: filter, select: { ...preValueSelect, [idField.name]: true } }; this.logger.info(`fetching pre-update entities for ${model}: ${format(query)})}`); @@ -543,11 +579,18 @@ export class PolicyUtil { ) { this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`); - const count = (await db[model].count({ where: filter })) as number; + const queryFilter = deepcopy(filter); + + // query args will be used with findMany, so we need to + // translate unique constraint filters into a flat filter + // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } + await this.flattenGeneratedUniqueField(model, queryFilter); + + const count = (await db[model].count({ where: queryFilter })) as number; const guard = await this.getAuthGuard(model, operation); // build a query condition with policy injected - const guardedQuery = { where: this.and(filter, guard) }; + const guardedQuery = { where: this.and(queryFilter, guard) }; const schema = (operation === 'create' || operation === 'update') && (await this.getModelSchema(model)); diff --git a/packages/runtime/src/enhancements/types.ts b/packages/runtime/src/enhancements/types.ts index 73364e7cc..8f81a3887 100644 --- a/packages/runtime/src/enhancements/types.ts +++ b/packages/runtime/src/enhancements/types.ts @@ -1,10 +1,19 @@ import { z } from 'zod'; import { FieldInfo, PolicyOperationKind, QueryContext } from '../types'; +/** + * Metadata for a model-level unique constraint + * e.g.: @@unique([a, b]) + */ +export type UniqueConstraint = { name: string; fields: string[] }; + /** * ZModel data model metadata */ -export type ModelMeta = { fields: Record> }; +export type ModelMeta = { + fields: Record>; + uniqueConstraints: Record>; +}; /** * Function for getting policy guard with a given context diff --git a/packages/schema/package.json b/packages/schema/package.json index d803e6250..84f8be89c 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.31", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/plugins/model-meta/index.ts b/packages/schema/src/plugins/model-meta/index.ts index ba708c951..13794805a 100644 --- a/packages/schema/src/plugins/model-meta/index.ts +++ b/packages/schema/src/plugins/model-meta/index.ts @@ -1,6 +1,14 @@ -import { DataModel, DataModelField, Model, isDataModel, isLiteralExpr } from '@zenstackhq/language/ast'; +import { + ArrayExpr, + DataModel, + DataModelField, + isDataModel, + isLiteralExpr, + Model, + ReferenceExpr, +} from '@zenstackhq/language/ast'; import { RuntimeAttribute } from '@zenstackhq/runtime'; -import { PluginOptions, getLiteral, resolved } from '@zenstackhq/sdk'; +import { getAttributeArgs, getLiteral, PluginOptions, resolved } from '@zenstackhq/sdk'; import { camelCase } from 'change-case'; import path from 'path'; import { CodeBlockWriter, Project, VariableDeclarationKind } from 'ts-morph'; @@ -66,6 +74,23 @@ function generateModelMetadata(dataModels: DataModel[], writer: CodeBlockWriter) } }); writer.write(','); + + writer.write('uniqueConstraints:'); + writer.block(() => { + for (const model of dataModels) { + writer.write(`${camelCase(model.name)}:`); + writer.block(() => { + for (const constraint of getUniqueConstraints(model)) { + writer.write(`${constraint.name}: { + name: "${constraint.name}", + fields: ${JSON.stringify(constraint.fields)} + },`); + } + }); + writer.write(','); + } + }); + writer.write(','); }); } @@ -119,3 +144,22 @@ function getFieldAttributes(field: DataModelField): RuntimeAttribute[] { function isIdField(field: DataModelField) { return field.attributes.some((attr) => attr.decl.ref?.name === '@id'); } + +function getUniqueConstraints(model: DataModel) { + const constraints: Array<{ name: string; fields: string[] }> = []; + for (const attr of model.attributes.filter((attr) => attr.decl.ref?.name === '@@unique')) { + const argsMap = getAttributeArgs(attr); + if (argsMap.fields) { + const fieldNames = (argsMap.fields as ArrayExpr).items.map( + (item) => resolved((item as ReferenceExpr).target).name + ); + let constraintName = argsMap.name && getLiteral(argsMap.name); + if (!constraintName) { + // default constraint name is fields concatenated with underscores + constraintName = fieldNames.join('_'); + } + constraints.push({ name: constraintName, fields: fieldNames }); + } + } + return constraints; +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2311c713c..018906cba 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.28", + "version": "1.0.0-alpha.31", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index d01f7ffe0..095ba6c46 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -1,4 +1,12 @@ -import { AstNode, Expression, isArrayExpr, isLiteralExpr, Reference } from '@zenstackhq/language/ast'; +import { + AstNode, + DataModelAttribute, + DataModelFieldAttribute, + Expression, + isArrayExpr, + isLiteralExpr, + Reference, +} from '@zenstackhq/language/ast'; export function resolved(ref: Reference): T { if (!ref.ref) { @@ -36,3 +44,14 @@ export default function indentString(string: string, count = 4): string { const indent = ' '; return string.replace(/^(?!\s*$)/gm, indent.repeat(count)); } + +export function getAttributeArgs(attr: DataModelAttribute | DataModelFieldAttribute): Record { + const result: Record = {}; + for (const arg of attr.args) { + if (!arg.$resolvedParam) { + continue; + } + result[arg.$resolvedParam.name] = arg.value; + } + return result; +} diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 17a748a13..43ae64eac 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -126,7 +126,7 @@ }, "../../../packages/runtime/dist": { "name": "@zenstackhq/runtime", - "version": "1.0.0-alpha.25", + "version": "1.0.0-alpha.31", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -160,7 +160,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.25", + "version": "1.0.0-alpha.31", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/tests/integration/tests/with-policy/multi-field-unique.test.ts b/tests/integration/tests/with-policy/multi-field-unique.test.ts new file mode 100644 index 000000000..d9c02b4e2 --- /dev/null +++ b/tests/integration/tests/with-policy/multi-field-unique.test.ts @@ -0,0 +1,193 @@ +import path from 'path'; +import { MODEL_PRELUDE, loadPrisma, run } from '../../utils'; + +describe('With Policy: multi-field unique', () => { + let origDir: string; + const suite = 'multi-field-unique'; + + beforeAll(async () => { + origDir = path.resolve('.'); + }); + + afterEach(() => { + process.chdir(origDir); + }); + + it('toplevel crud test unnamed constraint', async () => { + const { withPolicy } = await loadPrisma( + `${suite}/toplevel-crud-unnamed`, + ` + ${MODEL_PRELUDE} + + model Model { + id String @id @default(uuid()) + a String + b String + x Int + @@unique([a, b]) + + @@allow('all', x > 0) + @@deny('update', future().x <= 0) + } + ` + ); + + const db = withPolicy(); + + await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy(); + await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 2 } })).toBeRejectedWithCode('P2002'); + await expect(db.model.create({ data: { a: 'a2', b: 'b2', x: 0 } })).toBeRejectedByPolicy(); + + await expect(db.model.findUnique({ where: { a_b: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); + await expect(db.model.findUnique({ where: { a_b: { a: 'a1', b: 'b2' } } })).toResolveFalsy(); + await expect(db.model.update({ where: { a_b: { a: 'a1', b: 'b1' } }, data: { x: 2 } })).toResolveTruthy(); + await expect(db.model.update({ where: { a_b: { a: 'a1', b: 'b1' } }, data: { x: 0 } })).toBeRejectedByPolicy(); + + await expect(db.model.delete({ where: { a_b: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); + }); + + it('toplevel crud test named constraint', async () => { + const { withPolicy } = await loadPrisma( + `${suite}/toplevel-crud-named`, + ` + ${MODEL_PRELUDE} + + model Model { + id String @id @default(uuid()) + a String + b String + x Int + @@unique([a, b], name: 'myconstraint') + + @@allow('all', x > 0) + @@deny('update', future().x <= 0) + } + ` + ); + + const db = withPolicy(); + + await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy(); + await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); + await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b2' } } })).toResolveFalsy(); + await expect( + db.model.update({ where: { myconstraint: { a: 'a1', b: 'b1' } }, data: { x: 2 } }) + ).toResolveTruthy(); + await expect( + db.model.update({ where: { myconstraint: { a: 'a1', b: 'b1' } }, data: { x: 0 } }) + ).toBeRejectedByPolicy(); + await expect(db.model.delete({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); + }); + + it('nested crud test', async () => { + const { withPolicy } = await loadPrisma( + `${suite}/nested-crud`, + ` + ${MODEL_PRELUDE} + + model M1 { + id String @id @default(uuid()) + m2 M2[] + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + a String + b String + x Int + m1 M1 @relation(fields: [m1Id], references: [id]) + m1Id String + + @@unique([a, b]) + @@allow('all', x > 0) + } + ` + ); + + const db = withPolicy(); + + await expect(db.m1.create({ data: { id: '1', m2: { create: { a: 'a1', b: 'b1', x: 1 } } } })).toResolveTruthy(); + await expect( + db.m1.create({ data: { id: '2', m2: { create: { a: 'a1', b: 'b1', x: 2 } } } }) + ).toBeRejectedWithCode('P2002'); + await expect( + db.m1.create({ data: { id: '3', m2: { create: { a: 'a1', b: 'b2', x: 0 } } } }) + ).toBeRejectedByPolicy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + connectOrCreate: { + where: { a_b: { a: 'a1', b: 'b1' } }, + create: { a: 'a1', b: 'b1', x: 2 }, + }, + }, + }, + }) + ).toResolveTruthy(); + await expect(db.m2.count()).resolves.toBe(1); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + connectOrCreate: { + where: { a_b: { a: 'a1', b: 'b2' } }, + create: { a: 'a1', b: 'b2', x: 2 }, + }, + }, + }, + }) + ).toResolveTruthy(); + await expect(db.m2.count()).resolves.toBe(2); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + connectOrCreate: { + where: { a_b: { a: 'a2', b: 'b2' } }, + create: { a: 'a2', b: 'b2', x: 0 }, + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { + where: { a_b: { a: 'a1', b: 'b2' } }, + data: { x: 3 }, + }, + }, + }, + }) + ).toResolveTruthy(); + await expect(db.m2.findUnique({ where: { a_b: { a: 'a1', b: 'b2' } } })).resolves.toEqual( + expect.objectContaining({ x: 3 }) + ); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + delete: { + a_b: { a: 'a1', b: 'b1' }, + }, + }, + }, + }) + ).toResolveTruthy(); + await expect(db.m2.count()).resolves.toBe(1); + }); +});