diff --git a/README.md b/README.md
index 070b960e5..75cab857b 100644
--- a/README.md
+++ b/README.md
@@ -158,7 +158,7 @@ The following diagram gives a high-level architecture overview of ZenStack.
### Plugins
- Prisma schema generator
-- Zod schema generator
+- [Zod](https://zod.dev/) schema generator
- [SWR](https://github.com/vercel/swr) and [TanStack Query](https://github.com/TanStack/query) hooks generator
- OpenAPI specification generator
- [tRPC](https://trpc.io) router generator
@@ -166,7 +166,7 @@ The following diagram gives a high-level architecture overview of ZenStack.
### Framework adapters
-- [Next.js](https://zenstack.dev/docs/reference/server-adapters/next) (including support for the new "app directory" in Next.js 13)
+- [Next.js](https://zenstack.dev/docs/reference/server-adapters/next)
- [Nuxt](https://zenstack.dev/docs/reference/server-adapters/nuxt)
- [SvelteKit](https://zenstack.dev/docs/reference/server-adapters/sveltekit)
- [Fastify](https://zenstack.dev/docs/reference/server-adapters/fastify)
@@ -180,7 +180,7 @@ The following diagram gives a high-level architecture overview of ZenStack.
- [Custom attributes and functions](https://zenstack.dev/docs/reference/zmodel-language#custom-attributes-and-functions)
- [Multi-file schema and model inheritance](https://zenstack.dev/docs/guides/multiple-schema)
- [Polymorphic Relations](https://zenstack.dev/docs/guides/polymorphism)
-- Strong-typed JSON field (coming soon)
+- [Strongly typed JSON field](https://zenstack.dev/docs/guides/typing-json)
- 🙋🏻 [Request for an extension](https://discord.gg/Ykhr738dUe)
## Examples
@@ -200,19 +200,19 @@ You can use [this blog post](https://zenstack.dev/blog/model-authz) as an introd
Check out the [Multi-tenant Todo App](https://zenstack-todo.vercel.app/) for a running example. You can find different implementations below:
-- [Next.js 13 + NextAuth + SWR](https://github.com/zenstackhq/sample-todo-nextjs)
-- [Next.js 13 + NextAuth + TanStack Query](https://github.com/zenstackhq/sample-todo-nextjs-tanstack)
-- [Next.js 13 + NextAuth + tRPC](https://github.com/zenstackhq/sample-todo-trpc)
-- [Nuxt V3 + TanStack Query](https://github.com/zenstackhq/sample-todo-nuxt)
+- [Next.js + NextAuth + TanStack Query](https://github.com/zenstackhq/sample-todo-nextjs-tanstack)
+- [Next.js + NextAuth + SWR](https://github.com/zenstackhq/sample-todo-nextjs)
+- [Next.js + NextAuth + tRPC](https://github.com/zenstackhq/sample-todo-trpc)
+- [Nuxt + TanStack Query](https://github.com/zenstackhq/sample-todo-nuxt)
- [SvelteKit + TanStack Query](https://github.com/zenstackhq/sample-todo-sveltekit)
- [RedwoodJS](https://github.com/zenstackhq/sample-todo-redwood)
### Blog App
-- [Next.js 13 + Pages Route + SWR](https://github.com/zenstackhq/docs-tutorial-nextjs)
-- [Next.js 13 + App Route + ReactQuery](https://github.com/zenstackhq/docs-tutorial-nextjs-app-dir)
-- [Next.js 13 + App Route + tRPC](https://github.com/zenstackhq/sample-blog-nextjs-app-trpc)
-- [Nuxt V3 + TanStack Query](https://github.com/zenstackhq/docs-tutorial-nuxt)
+- [Next.js + App Route + TanStack Query](https://github.com/zenstackhq/docs-tutorial-nextjs-app-dir)
+- [Next.js + Pages Route + SWR](https://github.com/zenstackhq/docs-tutorial-nextjs)
+- [Next.js + App Route + tRPC](https://github.com/zenstackhq/sample-blog-nextjs-app-trpc)
+- [Nuxt + TanStack Query](https://github.com/zenstackhq/docs-tutorial-nuxt)
- [SvelteKit](https://github.com/zenstackhq/docs-tutorial-sveltekit)
- [Remix](https://github.com/zenstackhq/docs-tutorial-remix)
- [NestJS Backend API](https://github.com/zenstackhq/docs-tutorial-nestjs)
@@ -225,7 +225,7 @@ Join our [discord server](https://discord.gg/Ykhr738dUe) for chat and updates!
## Contributing
-If you like ZenStack, join us to make it a better tool! Please use the [Contributing Guide](CONTRIBUTING.md) for details on how to get started, and don't hesitate to join [Discord](https://discord.gg/Ykhr738dUe) to share your thoughts.
+If you like ZenStack, join us to make it a better tool! Please use the [Contributing Guide](CONTRIBUTING.md) for details on how to get started, and don't hesitate to join [Discord](https://discord.gg/Ykhr738dUe) to share your thoughts. Documentations reside in a separate repo: [zenstack-docs](https://github.com/zenstackhq/zenstack-docs).
Please also consider [sponsoring our work](https://github.com/sponsors/zenstackhq) to speed up the development. Your contribution will be 100% used as a bounty reward to encourage community members to help fix bugs, add features, and improve documentation.
@@ -241,7 +241,6 @@ Thank you for your generous support!
Mermaid Chart |
CodeRabbit |
Johann Rohn |
- Benjamin Zecirovic |
@@ -249,6 +248,7 @@ Thank you for your generous support!
+ Benjamin Zecirovic |
Ulric |
Fabian Jocks |
diff --git a/package.json b/package.json
index 5865b0c14..519137c35 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "zenstack-monorepo",
- "version": "2.7.5",
+ "version": "2.8.0",
"description": "",
"scripts": {
"build": "pnpm -r build",
diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md
index 7587a190d..75dd964c5 100644
--- a/packages/ide/jetbrains/CHANGELOG.md
+++ b/packages/ide/jetbrains/CHANGELOG.md
@@ -2,6 +2,12 @@
## [Unreleased]
+### Added
+
+- Type declaration support.
+
+## 2.7.0
+
### Fixed
- ZModel validation issues importing zmodel files from npm packages.
diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts
index 0fc8a6f16..067e6567e 100644
--- a/packages/ide/jetbrains/build.gradle.kts
+++ b/packages/ide/jetbrains/build.gradle.kts
@@ -9,7 +9,7 @@ plugins {
}
group = "dev.zenstack"
-version = "2.7.5"
+version = "2.8.0"
repositories {
mavenCentral()
diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json
index 76cfe0e0c..372462b17 100644
--- a/packages/ide/jetbrains/package.json
+++ b/packages/ide/jetbrains/package.json
@@ -1,6 +1,6 @@
{
"name": "jetbrains",
- "version": "2.7.5",
+ "version": "2.8.0",
"displayName": "ZenStack JetBrains IDE Plugin",
"description": "ZenStack JetBrains IDE plugin",
"homepage": "https://zenstack.dev",
diff --git a/packages/language/package.json b/packages/language/package.json
index 03d937974..cf01ce9ad 100644
--- a/packages/language/package.json
+++ b/packages/language/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/language",
- "version": "2.7.5",
+ "version": "2.8.0",
"displayName": "ZenStack modeling language compiler",
"description": "ZenStack modeling language compiler",
"homepage": "https://zenstack.dev",
diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts
index 98b461285..b4d7e6d82 100644
--- a/packages/language/src/generated/ast.ts
+++ b/packages/language/src/generated/ast.ts
@@ -20,7 +20,7 @@ export const ZModelTerminals = {
SL_COMMENT: /\/\/[^\n\r]*/,
};
-export type AbstractDeclaration = Attribute | DataModel | DataSource | Enum | FunctionDecl | GeneratorDecl | Plugin;
+export type AbstractDeclaration = Attribute | DataModel | DataSource | Enum | FunctionDecl | GeneratorDecl | Plugin | TypeDef;
export const AbstractDeclaration = 'AbstractDeclaration';
@@ -78,10 +78,10 @@ export function isReferenceTarget(item: unknown): item is ReferenceTarget {
return reflection.isInstance(item, ReferenceTarget);
}
-export type RegularID = 'abstract' | 'attribute' | 'datasource' | 'enum' | 'import' | 'in' | 'model' | 'plugin' | 'view' | string;
+export type RegularID = 'abstract' | 'attribute' | 'datasource' | 'enum' | 'import' | 'in' | 'model' | 'plugin' | 'type' | 'view' | string;
export function isRegularID(item: unknown): item is RegularID {
- return item === 'model' || item === 'enum' || item === 'attribute' || item === 'datasource' || item === 'plugin' || item === 'abstract' || item === 'in' || item === 'view' || item === 'import' || (typeof item === 'string' && (/[_a-zA-Z][\w_]*/.test(item)));
+ return item === 'model' || item === 'enum' || item === 'attribute' || item === 'datasource' || item === 'plugin' || item === 'abstract' || item === 'in' || item === 'view' || item === 'import' || item === 'type' || (typeof item === 'string' && (/[_a-zA-Z][\w_]*/.test(item)));
}
export type RegularIDWithTypeNames = 'Any' | 'BigInt' | 'Boolean' | 'Bytes' | 'DateTime' | 'Decimal' | 'Float' | 'Int' | 'Json' | 'Null' | 'Object' | 'String' | 'Unsupported' | RegularID;
@@ -90,7 +90,7 @@ export function isRegularIDWithTypeNames(item: unknown): item is RegularIDWithTy
return isRegularID(item) || item === 'String' || item === 'Boolean' || item === 'Int' || item === 'BigInt' || item === 'Float' || item === 'Decimal' || item === 'DateTime' || item === 'Json' || item === 'Bytes' || item === 'Null' || item === 'Object' || item === 'Any' || item === 'Unsupported';
}
-export type TypeDeclaration = DataModel | Enum;
+export type TypeDeclaration = DataModel | Enum | TypeDef;
export const TypeDeclaration = 'TypeDeclaration';
@@ -305,7 +305,7 @@ export function isDataModelField(item: unknown): item is DataModelField {
}
export interface DataModelFieldAttribute extends AstNode {
- readonly $container: DataModelField | EnumField;
+ readonly $container: DataModelField | EnumField | TypeDefField;
readonly $type: 'DataModelFieldAttribute';
args: Array
decl: Reference
@@ -620,6 +620,50 @@ export function isThisExpr(item: unknown): item is ThisExpr {
return reflection.isInstance(item, ThisExpr);
}
+export interface TypeDef extends AstNode {
+ readonly $container: Model;
+ readonly $type: 'TypeDef';
+ comments: Array
+ fields: Array
+ name: RegularID
+}
+
+export const TypeDef = 'TypeDef';
+
+export function isTypeDef(item: unknown): item is TypeDef {
+ return reflection.isInstance(item, TypeDef);
+}
+
+export interface TypeDefField extends AstNode {
+ readonly $container: TypeDef;
+ readonly $type: 'TypeDefField';
+ attributes: Array
+ comments: Array
+ name: RegularIDWithTypeNames
+ type: TypeDefFieldType
+}
+
+export const TypeDefField = 'TypeDefField';
+
+export function isTypeDefField(item: unknown): item is TypeDefField {
+ return reflection.isInstance(item, TypeDefField);
+}
+
+export interface TypeDefFieldType extends AstNode {
+ readonly $container: TypeDefField;
+ readonly $type: 'TypeDefFieldType';
+ array: boolean
+ optional: boolean
+ reference?: Reference
+ type?: BuiltinType
+}
+
+export const TypeDefFieldType = 'TypeDefFieldType';
+
+export function isTypeDefFieldType(item: unknown): item is TypeDefFieldType {
+ return reflection.isInstance(item, TypeDefFieldType);
+}
+
export interface UnaryExpr extends AstNode {
readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | ConfigArrayExpr | ConfigField | ConfigInvocationArg | FieldInitializer | FunctionDecl | MemberAccessExpr | PluginField | ReferenceArg | UnaryExpr | UnsupportedFieldType;
readonly $type: 'UnaryExpr';
@@ -691,6 +735,9 @@ export type ZModelAstType = {
StringLiteral: StringLiteral
ThisExpr: ThisExpr
TypeDeclaration: TypeDeclaration
+ TypeDef: TypeDef
+ TypeDefField: TypeDefField
+ TypeDefFieldType: TypeDefFieldType
UnaryExpr: UnaryExpr
UnsupportedFieldType: UnsupportedFieldType
}
@@ -698,7 +745,7 @@ export type ZModelAstType = {
export class ZModelAstReflection extends AbstractAstReflection {
getAllTypes(): string[] {
- return ['AbstractDeclaration', 'Argument', 'ArrayExpr', 'Attribute', 'AttributeArg', 'AttributeParam', 'AttributeParamType', 'BinaryExpr', 'BooleanLiteral', 'ConfigArrayExpr', 'ConfigExpr', 'ConfigField', 'ConfigInvocationArg', 'ConfigInvocationExpr', 'DataModel', 'DataModelAttribute', 'DataModelField', 'DataModelFieldAttribute', 'DataModelFieldType', 'DataSource', 'Enum', 'EnumField', 'Expression', 'FieldInitializer', 'FunctionDecl', 'FunctionParam', 'FunctionParamType', 'GeneratorDecl', 'InternalAttribute', 'InvocationExpr', 'LiteralExpr', 'MemberAccessExpr', 'Model', 'ModelImport', 'NullExpr', 'NumberLiteral', 'ObjectExpr', 'Plugin', 'PluginField', 'ReferenceArg', 'ReferenceExpr', 'ReferenceTarget', 'StringLiteral', 'ThisExpr', 'TypeDeclaration', 'UnaryExpr', 'UnsupportedFieldType'];
+ return ['AbstractDeclaration', 'Argument', 'ArrayExpr', 'Attribute', 'AttributeArg', 'AttributeParam', 'AttributeParamType', 'BinaryExpr', 'BooleanLiteral', 'ConfigArrayExpr', 'ConfigExpr', 'ConfigField', 'ConfigInvocationArg', 'ConfigInvocationExpr', 'DataModel', 'DataModelAttribute', 'DataModelField', 'DataModelFieldAttribute', 'DataModelFieldType', 'DataSource', 'Enum', 'EnumField', 'Expression', 'FieldInitializer', 'FunctionDecl', 'FunctionParam', 'FunctionParamType', 'GeneratorDecl', 'InternalAttribute', 'InvocationExpr', 'LiteralExpr', 'MemberAccessExpr', 'Model', 'ModelImport', 'NullExpr', 'NumberLiteral', 'ObjectExpr', 'Plugin', 'PluginField', 'ReferenceArg', 'ReferenceExpr', 'ReferenceTarget', 'StringLiteral', 'ThisExpr', 'TypeDeclaration', 'TypeDef', 'TypeDefField', 'TypeDefFieldType', 'UnaryExpr', 'UnsupportedFieldType'];
}
protected override computeIsSubtype(subtype: string, supertype: string): boolean {
@@ -729,7 +776,8 @@ export class ZModelAstReflection extends AbstractAstReflection {
return this.isSubtype(ConfigExpr, supertype);
}
case DataModel:
- case Enum: {
+ case Enum:
+ case TypeDef: {
return this.isSubtype(AbstractDeclaration, supertype) || this.isSubtype(TypeDeclaration, supertype);
}
case DataModelField:
@@ -772,6 +820,9 @@ export class ZModelAstReflection extends AbstractAstReflection {
case 'ReferenceExpr:target': {
return ReferenceTarget;
}
+ case 'TypeDefFieldType:reference': {
+ return TypeDef;
+ }
default: {
throw new Error(`${referenceId} is not a valid reference id.`);
}
@@ -989,6 +1040,33 @@ export class ZModelAstReflection extends AbstractAstReflection {
]
};
}
+ case 'TypeDef': {
+ return {
+ name: 'TypeDef',
+ mandatory: [
+ { name: 'comments', type: 'array' },
+ { name: 'fields', type: 'array' }
+ ]
+ };
+ }
+ case 'TypeDefField': {
+ return {
+ name: 'TypeDefField',
+ mandatory: [
+ { name: 'attributes', type: 'array' },
+ { name: 'comments', type: 'array' }
+ ]
+ };
+ }
+ case 'TypeDefFieldType': {
+ return {
+ name: 'TypeDefFieldType',
+ mandatory: [
+ { name: 'array', type: 'boolean' },
+ { name: 'optional', type: 'boolean' }
+ ]
+ };
+ }
default: {
return {
name: type,
diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts
index 3b172570d..01c492bd5 100644
--- a/packages/language/src/generated/grammar.ts
+++ b/packages/language/src/generated/grammar.ts
@@ -70,7 +70,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@64"
+ "$ref": "#/rules@67"
},
"arguments": []
}
@@ -126,21 +126,28 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@41"
+ "$ref": "#/rules@40"
},
"arguments": []
},
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@43"
+ "$ref": "#/rules@44"
},
"arguments": []
},
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@48"
+ "$ref": "#/rules@46"
+ },
+ "arguments": []
+ },
+ {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@51"
},
"arguments": []
}
@@ -162,7 +169,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": [],
"cardinality": "*"
@@ -178,7 +185,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -222,7 +229,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": [],
"cardinality": "*"
@@ -238,7 +245,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -282,7 +289,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": [],
"cardinality": "*"
@@ -294,7 +301,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -333,7 +340,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": [],
"cardinality": "*"
@@ -349,7 +356,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -393,7 +400,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": [],
"cardinality": "*"
@@ -405,7 +412,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -481,7 +488,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@65"
+ "$ref": "#/rules@68"
},
"arguments": []
}
@@ -503,7 +510,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@64"
+ "$ref": "#/rules@67"
},
"arguments": []
}
@@ -525,7 +532,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@58"
+ "$ref": "#/rules@61"
},
"arguments": []
}
@@ -649,7 +656,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@63"
+ "$ref": "#/rules@66"
},
"arguments": []
}
@@ -747,7 +754,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@63"
+ "$ref": "#/rules@66"
},
"arguments": []
}
@@ -956,7 +963,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@47"
+ "$ref": "#/rules@50"
},
"arguments": []
},
@@ -1055,7 +1062,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@63"
+ "$ref": "#/rules@66"
},
"arguments": []
}
@@ -1169,14 +1176,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
},
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@64"
+ "$ref": "#/rules@67"
},
"arguments": []
}
@@ -1221,7 +1228,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "CrossReference",
"type": {
- "$ref": "#/rules@43"
+ "$ref": "#/rules@46"
},
"deprecatedSyntax": false
}
@@ -1894,7 +1901,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": []
},
@@ -1927,7 +1934,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -1997,7 +2004,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -2032,7 +2039,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@52"
+ "$ref": "#/rules@55"
},
"arguments": []
}
@@ -2066,7 +2073,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": []
},
@@ -2079,7 +2086,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@47"
+ "$ref": "#/rules@50"
},
"arguments": []
}
@@ -2103,7 +2110,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@51"
+ "$ref": "#/rules@54"
},
"arguments": []
},
@@ -2134,7 +2141,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@57"
+ "$ref": "#/rules@60"
},
"arguments": []
}
@@ -2146,7 +2153,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@40"
+ "$ref": "#/rules@43"
},
"arguments": []
}
@@ -2163,7 +2170,217 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
+ },
+ "arguments": []
+ },
+ "deprecatedSyntax": false
+ }
+ }
+ ]
+ },
+ {
+ "$type": "Group",
+ "elements": [
+ {
+ "$type": "Assignment",
+ "feature": "array",
+ "operator": "?=",
+ "terminal": {
+ "$type": "Keyword",
+ "value": "["
+ }
+ },
+ {
+ "$type": "Keyword",
+ "value": "]"
+ }
+ ],
+ "cardinality": "?"
+ },
+ {
+ "$type": "Assignment",
+ "feature": "optional",
+ "operator": "?=",
+ "terminal": {
+ "$type": "Keyword",
+ "value": "?"
+ },
+ "cardinality": "?"
+ }
+ ]
+ },
+ "definesHiddenTokens": false,
+ "entry": false,
+ "fragment": false,
+ "hiddenTokens": [],
+ "parameters": [],
+ "wildcard": false
+ },
+ {
+ "$type": "ParserRule",
+ "name": "TypeDef",
+ "definition": {
+ "$type": "Group",
+ "elements": [
+ {
+ "$type": "Assignment",
+ "feature": "comments",
+ "operator": "+=",
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@69"
+ },
+ "arguments": []
+ },
+ "cardinality": "*"
+ },
+ {
+ "$type": "Keyword",
+ "value": "type"
+ },
+ {
+ "$type": "Assignment",
+ "feature": "name",
+ "operator": "=",
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@49"
+ },
+ "arguments": []
+ }
+ },
+ {
+ "$type": "Keyword",
+ "value": "{"
+ },
+ {
+ "$type": "Assignment",
+ "feature": "fields",
+ "operator": "+=",
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@41"
+ },
+ "arguments": []
+ },
+ "cardinality": "+"
+ },
+ {
+ "$type": "Keyword",
+ "value": "}"
+ }
+ ]
+ },
+ "definesHiddenTokens": false,
+ "entry": false,
+ "fragment": false,
+ "hiddenTokens": [],
+ "parameters": [],
+ "wildcard": false
+ },
+ {
+ "$type": "ParserRule",
+ "name": "TypeDefField",
+ "definition": {
+ "$type": "Group",
+ "elements": [
+ {
+ "$type": "Assignment",
+ "feature": "comments",
+ "operator": "+=",
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@69"
+ },
+ "arguments": []
+ },
+ "cardinality": "*"
+ },
+ {
+ "$type": "Assignment",
+ "feature": "name",
+ "operator": "=",
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@50"
+ },
+ "arguments": []
+ }
+ },
+ {
+ "$type": "Assignment",
+ "feature": "type",
+ "operator": "=",
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@42"
+ },
+ "arguments": []
+ }
+ },
+ {
+ "$type": "Assignment",
+ "feature": "attributes",
+ "operator": "+=",
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@54"
+ },
+ "arguments": []
+ },
+ "cardinality": "*"
+ }
+ ]
+ },
+ "definesHiddenTokens": false,
+ "entry": false,
+ "fragment": false,
+ "hiddenTokens": [],
+ "parameters": [],
+ "wildcard": false
+ },
+ {
+ "$type": "ParserRule",
+ "name": "TypeDefFieldType",
+ "definition": {
+ "$type": "Group",
+ "elements": [
+ {
+ "$type": "Alternatives",
+ "elements": [
+ {
+ "$type": "Assignment",
+ "feature": "type",
+ "operator": "=",
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@60"
+ },
+ "arguments": []
+ }
+ },
+ {
+ "$type": "Assignment",
+ "feature": "reference",
+ "operator": "=",
+ "terminal": {
+ "$type": "CrossReference",
+ "type": {
+ "$ref": "#/rules@40"
+ },
+ "terminal": {
+ "$type": "RuleCall",
+ "rule": {
+ "$ref": "#/rules@49"
},
"arguments": []
},
@@ -2262,7 +2479,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": []
},
@@ -2279,7 +2496,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -2298,7 +2515,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@42"
+ "$ref": "#/rules@45"
},
"arguments": []
}
@@ -2310,7 +2527,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@52"
+ "$ref": "#/rules@55"
},
"arguments": []
}
@@ -2344,7 +2561,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": []
},
@@ -2357,7 +2574,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@47"
+ "$ref": "#/rules@50"
},
"arguments": []
}
@@ -2369,7 +2586,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@51"
+ "$ref": "#/rules@54"
},
"arguments": []
},
@@ -2393,7 +2610,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": [],
"cardinality": "*"
@@ -2409,7 +2626,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -2428,7 +2645,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@44"
+ "$ref": "#/rules@47"
},
"arguments": []
}
@@ -2447,7 +2664,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@44"
+ "$ref": "#/rules@47"
},
"arguments": []
}
@@ -2473,7 +2690,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@45"
+ "$ref": "#/rules@48"
},
"arguments": []
}
@@ -2506,7 +2723,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@53"
+ "$ref": "#/rules@56"
},
"arguments": []
},
@@ -2530,7 +2747,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": [],
"cardinality": "*"
@@ -2542,7 +2759,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -2558,7 +2775,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@45"
+ "$ref": "#/rules@48"
},
"arguments": []
}
@@ -2598,7 +2815,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@56"
+ "$ref": "#/rules@59"
},
"arguments": []
}
@@ -2615,7 +2832,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
},
@@ -2662,7 +2879,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@63"
+ "$ref": "#/rules@66"
},
"arguments": []
},
@@ -2701,6 +2918,10 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "Keyword",
"value": "import"
+ },
+ {
+ "$type": "Keyword",
+ "value": "type"
}
]
},
@@ -2721,7 +2942,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
},
@@ -2799,7 +3020,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": []
},
@@ -2819,21 +3040,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@60"
+ "$ref": "#/rules@63"
},
"arguments": []
},
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@61"
+ "$ref": "#/rules@64"
},
"arguments": []
},
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@62"
+ "$ref": "#/rules@65"
},
"arguments": []
}
@@ -2854,7 +3075,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@49"
+ "$ref": "#/rules@52"
},
"arguments": []
}
@@ -2873,7 +3094,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@49"
+ "$ref": "#/rules@52"
},
"arguments": []
}
@@ -2895,7 +3116,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@53"
+ "$ref": "#/rules@56"
},
"arguments": []
},
@@ -2923,7 +3144,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": []
},
@@ -2946,7 +3167,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -2962,7 +3183,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@50"
+ "$ref": "#/rules@53"
},
"arguments": []
}
@@ -2974,7 +3195,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@53"
+ "$ref": "#/rules@56"
},
"arguments": []
},
@@ -3008,7 +3229,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@56"
+ "$ref": "#/rules@59"
},
"arguments": []
},
@@ -3039,7 +3260,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
},
@@ -3099,12 +3320,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "CrossReference",
"type": {
- "$ref": "#/rules@48"
+ "$ref": "#/rules@51"
},
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@62"
+ "$ref": "#/rules@65"
},
"arguments": []
},
@@ -3121,7 +3342,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@54"
+ "$ref": "#/rules@57"
},
"arguments": [],
"cardinality": "?"
@@ -3151,7 +3372,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@66"
+ "$ref": "#/rules@69"
},
"arguments": [],
"cardinality": "*"
@@ -3163,12 +3384,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "CrossReference",
"type": {
- "$ref": "#/rules@48"
+ "$ref": "#/rules@51"
},
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@61"
+ "$ref": "#/rules@64"
},
"arguments": []
},
@@ -3185,7 +3406,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@54"
+ "$ref": "#/rules@57"
},
"arguments": [],
"cardinality": "?"
@@ -3219,12 +3440,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "CrossReference",
"type": {
- "$ref": "#/rules@48"
+ "$ref": "#/rules@51"
},
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@60"
+ "$ref": "#/rules@63"
},
"arguments": []
},
@@ -3241,7 +3462,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@54"
+ "$ref": "#/rules@57"
},
"arguments": [],
"cardinality": "?"
@@ -3276,7 +3497,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@55"
+ "$ref": "#/rules@58"
},
"arguments": []
}
@@ -3295,7 +3516,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@55"
+ "$ref": "#/rules@58"
},
"arguments": []
}
@@ -3327,7 +3548,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
"terminal": {
"$type": "RuleCall",
"rule": {
- "$ref": "#/rules@46"
+ "$ref": "#/rules@49"
},
"arguments": []
}
@@ -3599,7 +3820,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "SimpleType",
"typeRef": {
- "$ref": "#/rules@44"
+ "$ref": "#/rules@47"
}
},
{
@@ -3611,7 +3832,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "SimpleType",
"typeRef": {
- "$ref": "#/rules@42"
+ "$ref": "#/rules@45"
}
}
]
@@ -3632,7 +3853,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
{
"$type": "SimpleType",
"typeRef": {
- "$ref": "#/rules@41"
+ "$ref": "#/rules@40"
+ }
+ },
+ {
+ "$type": "SimpleType",
+ "typeRef": {
+ "$ref": "#/rules@44"
}
}
]
diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium
index 4f12159d6..d66ea5f32 100644
--- a/packages/language/src/zmodel.langium
+++ b/packages/language/src/zmodel.langium
@@ -11,7 +11,7 @@ ModelImport:
'import' path=STRING ';'?;
AbstractDeclaration:
- DataSource | GeneratorDecl| Plugin | DataModel | Enum | FunctionDecl | Attribute;
+ DataSource | GeneratorDecl| Plugin | DataModel | TypeDef | Enum | FunctionDecl | Attribute;
// datasource
DataSource:
@@ -113,22 +113,6 @@ CollectionPredicateExpr infers Expression:
'[' right=Expression ']'
)*;
-// TODO: support arithmetics?
-//
-// MultDivExpr infers Expression:
-// CollectionPredicateExpr (
-// {infer BinaryExpr.left=current}
-// operator=('*'|'/')
-// right=CollectionPredicateExpr
-// )*;
-
-// AddSubExpr infers Expression:
-// MultDivExpr (
-// {infer BinaryExpr.left=current}
-// operator=('+'|'-')
-// right=MultDivExpr
-// )*;
-
InExpr infers Expression:
CollectionPredicateExpr (
{infer BinaryExpr.left=current}
@@ -195,6 +179,20 @@ DataModelField:
DataModelFieldType:
(type=BuiltinType | unsupported=UnsupportedFieldType | reference=[TypeDeclaration:RegularID]) (array?='[' ']')? (optional?='?')?;
+TypeDef:
+ (comments+=TRIPLE_SLASH_COMMENT)*
+ 'type' name=RegularID '{' (
+ fields+=TypeDefField
+ )+
+ '}';
+
+TypeDefField:
+ (comments+=TRIPLE_SLASH_COMMENT)*
+ name=RegularIDWithTypeNames type=TypeDefFieldType (attributes+=DataModelFieldAttribute)*;
+
+TypeDefFieldType:
+ (type=BuiltinType | reference=[TypeDef:RegularID]) (array?='[' ']')? (optional?='?')?;
+
UnsupportedFieldType:
'Unsupported' '(' (value=LiteralExpr) ')';
@@ -224,7 +222,7 @@ FunctionParamType:
// https://github.com/langium/langium/discussions/1012
RegularID returns string:
// include keywords that we'd like to work as ID in most places
- ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import';
+ ID | 'model' | 'enum' | 'attribute' | 'datasource' | 'plugin' | 'abstract' | 'in' | 'view' | 'import' | 'type';
RegularIDWithTypeNames returns string:
RegularID | 'String' | 'Boolean' | 'Int' | 'BigInt' | 'Float' | 'Decimal' | 'DateTime' | 'Json' | 'Bytes' | 'Null' | 'Object' | 'Any' | 'Unsupported';
@@ -241,7 +239,7 @@ AttributeParam:
AttributeParamType:
(type=(ExpressionType | 'FieldReference' | 'TransitiveFieldReference' | 'ContextType') | reference=[TypeDeclaration:RegularID]) (array?='[' ']')? (optional?='?')?;
-type TypeDeclaration = DataModel | Enum;
+type TypeDeclaration = DataModel | TypeDef | Enum;
DataModelFieldAttribute:
decl=[Attribute:FIELD_ATTRIBUTE_NAME] ('(' AttributeArgList? ')')?;
diff --git a/packages/language/syntaxes/zmodel.tmLanguage b/packages/language/syntaxes/zmodel.tmLanguage
index 6102b919d..40b92fb9a 100644
--- a/packages/language/syntaxes/zmodel.tmLanguage
+++ b/packages/language/syntaxes/zmodel.tmLanguage
@@ -20,7 +20,7 @@
name
keyword.control.zmodel
match
- \b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|view)\b
+ \b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\b
name
diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json
index aad6a38c7..0fb0227e5 100644
--- a/packages/language/syntaxes/zmodel.tmLanguage.json
+++ b/packages/language/syntaxes/zmodel.tmLanguage.json
@@ -10,7 +10,7 @@
},
{
"name": "keyword.control.zmodel",
- "match": "\\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|view)\\b"
+ "match": "\\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\\b"
},
{
"name": "string.quoted.double.zmodel",
diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json
index 41edf7ead..554556519 100644
--- a/packages/misc/redwood/package.json
+++ b/packages/misc/redwood/package.json
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/redwood",
"displayName": "ZenStack RedwoodJS Integration",
- "version": "2.7.5",
+ "version": "2.8.0",
"description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.",
"repository": {
"type": "git",
diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json
index 870667ded..41cfaf7eb 100644
--- a/packages/plugins/openapi/package.json
+++ b/packages/plugins/openapi/package.json
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/openapi",
"displayName": "ZenStack Plugin and Runtime for OpenAPI",
- "version": "2.7.5",
+ "version": "2.8.0",
"description": "ZenStack plugin and runtime supporting OpenAPI",
"main": "index.js",
"repository": {
diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json
index 8cc181aa6..b72fb3810 100644
--- a/packages/plugins/swr/package.json
+++ b/packages/plugins/swr/package.json
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/swr",
"displayName": "ZenStack plugin for generating SWR hooks",
- "version": "2.7.5",
+ "version": "2.8.0",
"description": "ZenStack plugin for generating SWR hooks",
"main": "index.js",
"repository": {
diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts
index 46d18d296..967be9727 100644
--- a/packages/plugins/swr/src/generator.ts
+++ b/packages/plugins/swr/src/generator.ts
@@ -10,7 +10,7 @@ import {
resolvePath,
saveProject,
} from '@zenstackhq/sdk';
-import { DataModel, DataModelFieldType, Model, isEnum } from '@zenstackhq/sdk/ast';
+import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast';
import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma';
import { paramCase } from 'change-case';
import path from 'path';
@@ -28,8 +28,9 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
const warnings: string[] = [];
const models = getDataModels(model);
+ const typeDefs = model.declarations.filter(isTypeDef);
- await generateModelMeta(project, models, {
+ await generateModelMeta(project, models, typeDefs, {
output: path.join(outDir, '__model_meta.ts'),
generateAttributes: false,
});
diff --git a/packages/plugins/swr/src/runtime/index.ts b/packages/plugins/swr/src/runtime/index.ts
index 11e73692d..71004d66a 100644
--- a/packages/plugins/swr/src/runtime/index.ts
+++ b/packages/plugins/swr/src/runtime/index.ts
@@ -428,7 +428,7 @@ function marshal(value: unknown) {
function unmarshal(value: string) {
const parsed = JSON.parse(value);
- if (parsed.data && parsed.meta?.serialization) {
+ if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) {
const deserializedData = deserialize(parsed.data, parsed.meta.serialization);
return { ...parsed, data: deserializedData };
} else {
diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json
index ba0d1545b..4ac5a58bb 100644
--- a/packages/plugins/tanstack-query/package.json
+++ b/packages/plugins/tanstack-query/package.json
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/tanstack-query",
"displayName": "ZenStack plugin for generating tanstack-query hooks",
- "version": "2.7.5",
+ "version": "2.8.0",
"description": "ZenStack plugin for generating tanstack-query hooks",
"main": "index.js",
"exports": {
diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts
index afb86f9c7..8833f77f9 100644
--- a/packages/plugins/tanstack-query/src/generator.ts
+++ b/packages/plugins/tanstack-query/src/generator.ts
@@ -11,7 +11,7 @@ import {
resolvePath,
saveProject,
} from '@zenstackhq/sdk';
-import { DataModel, DataModelFieldType, Model, isEnum } from '@zenstackhq/sdk/ast';
+import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast';
import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma';
import { paramCase } from 'change-case';
import { lowerCaseFirst } from 'lower-case-first';
@@ -29,6 +29,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
const project = createProject();
const warnings: string[] = [];
const models = getDataModels(model);
+ const typeDefs = model.declarations.filter(isTypeDef);
const target = requireOption(options, 'target', name);
if (!supportedTargets.includes(target)) {
@@ -44,7 +45,7 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
outDir = resolvePath(outDir, options);
ensureEmptyDir(outDir);
- await generateModelMeta(project, models, {
+ await generateModelMeta(project, models, typeDefs, {
output: path.join(outDir, '__model_meta.ts'),
generateAttributes: false,
});
diff --git a/packages/plugins/tanstack-query/src/runtime/common.ts b/packages/plugins/tanstack-query/src/runtime/common.ts
index a595e3423..f63a235fe 100644
--- a/packages/plugins/tanstack-query/src/runtime/common.ts
+++ b/packages/plugins/tanstack-query/src/runtime/common.ts
@@ -221,7 +221,7 @@ export function marshal(value: unknown) {
export function unmarshal(value: string) {
const parsed = JSON.parse(value);
- if (parsed.data && parsed.meta?.serialization) {
+ if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) {
const deserializedData = deserialize(parsed.data, parsed.meta.serialization);
return { ...parsed, data: deserializedData };
} else {
diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json
index c62b0c70a..676f40b2c 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": "2.7.5",
+ "version": "2.8.0",
"description": "ZenStack plugin for tRPC",
"main": "index.js",
"repository": {
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index dcf4bb9ae..f63f40ced 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/runtime",
"displayName": "ZenStack Runtime Library",
- "version": "2.7.5",
+ "version": "2.8.0",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
@@ -111,7 +111,7 @@
"zod-validation-error": "^1.5.0"
},
"peerDependencies": {
- "@prisma/client": "5.0.0 - 5.21.x"
+ "@prisma/client": "5.0.0 - 5.22.x"
},
"author": {
"name": "ZenStack Team"
diff --git a/packages/runtime/src/cross/model-meta.ts b/packages/runtime/src/cross/model-meta.ts
index 3eb6b3786..3b27f1686 100644
--- a/packages/runtime/src/cross/model-meta.ts
+++ b/packages/runtime/src/cross/model-meta.ts
@@ -44,6 +44,11 @@ export type FieldInfo = {
*/
isDataModel?: boolean;
+ /**
+ * If the field type is a type def (or an optional/array of type def)
+ */
+ isTypeDef?: boolean;
+
/**
* If the field is an array
*/
@@ -143,6 +148,21 @@ export type ModelInfo = {
discriminator?: string;
};
+/**
+ * Metadata for a type def
+ */
+export type TypeDefInfo = {
+ /**
+ * TypeDef name
+ */
+ name: string;
+
+ /**
+ * Fields
+ */
+ fields: Record;
+};
+
/**
* ZModel data model metadata
*/
@@ -152,6 +172,11 @@ export type ModelMeta = {
*/
models: Record;
+ /**
+ * Type defs
+ */
+ typeDefs?: Record;
+
/**
* Mapping from model name to models that will be deleted because of it due to cascade delete
*/
@@ -171,15 +196,21 @@ export type ModelMeta = {
/**
* Resolves a model field to its metadata. Returns undefined if not found.
*/
-export function resolveField(modelMeta: ModelMeta, model: string, field: string): FieldInfo | undefined {
- return modelMeta.models[lowerCaseFirst(model)]?.fields?.[field];
+export function resolveField(
+ modelMeta: ModelMeta,
+ modelOrTypeDef: string,
+ field: string,
+ isTypeDef = false
+): FieldInfo | undefined {
+ const container = isTypeDef ? modelMeta.typeDefs : modelMeta.models;
+ return container?.[lowerCaseFirst(modelOrTypeDef)]?.fields?.[field];
}
/**
* Resolves a model field to its metadata. Throws an error if not found.
*/
-export function requireField(modelMeta: ModelMeta, model: string, field: string) {
- const f = resolveField(modelMeta, model, field);
+export function requireField(modelMeta: ModelMeta, model: string, field: string, isTypeDef = false) {
+ const f = resolveField(modelMeta, model, field, isTypeDef);
if (!f) {
throw new Error(`Field ${model}.${field} cannot be resolved`);
}
diff --git a/packages/runtime/src/cross/utils.ts b/packages/runtime/src/cross/utils.ts
index 304b9b618..b1cb67e12 100644
--- a/packages/runtime/src/cross/utils.ts
+++ b/packages/runtime/src/cross/utils.ts
@@ -1,5 +1,5 @@
import { lowerCaseFirst } from 'lower-case-first';
-import { requireField, type ModelInfo, type ModelMeta } from '.';
+import { requireField, type ModelInfo, type ModelMeta, type TypeDefInfo } from '.';
/**
* Gets field names in a data model entity, filtering out internal fields.
@@ -46,6 +46,9 @@ export function zip(x: Enumerable, y: Enumerable): Array<[T1, T2
}
}
+/**
+ * Gets ID fields of a model.
+ */
export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound = false) {
const uniqueConstraints = modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints ?? {};
@@ -60,6 +63,9 @@ export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound
return entries[0].fields.map((f) => requireField(modelMeta, model, f));
}
+/**
+ * Gets info for a model.
+ */
export function getModelInfo(
modelMeta: ModelMeta,
model: string,
@@ -72,6 +78,25 @@ export function getModelInfo(
return info;
}
+/**
+ * Gets info for a type def.
+ */
+export function getTypeDefInfo(
+ modelMeta: ModelMeta,
+ typeDef: string,
+ throwIfNotFound: Throw = false as Throw
+): Throw extends true ? TypeDefInfo : TypeDefInfo | undefined {
+ const info = modelMeta.typeDefs?.[lowerCaseFirst(typeDef)];
+ if (!info && throwIfNotFound) {
+ throw new Error(`Unable to load info for ${typeDef}`);
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return info as any;
+}
+
+/**
+ * Checks if a model is a delegate model.
+ */
export function isDelegateModel(modelMeta: ModelMeta, model: string) {
return !!getModelInfo(modelMeta, model)?.attributes?.some((attr) => attr.name === '@@delegate');
}
diff --git a/packages/runtime/src/enhancements/edge/json-processor.ts b/packages/runtime/src/enhancements/edge/json-processor.ts
new file mode 120000
index 000000000..4144fc6f4
--- /dev/null
+++ b/packages/runtime/src/enhancements/edge/json-processor.ts
@@ -0,0 +1 @@
+../node/json-processor.ts
\ No newline at end of file
diff --git a/packages/runtime/src/enhancements/node/create-enhancement.ts b/packages/runtime/src/enhancements/node/create-enhancement.ts
index 263e12192..adec1fdf2 100644
--- a/packages/runtime/src/enhancements/node/create-enhancement.ts
+++ b/packages/runtime/src/enhancements/node/create-enhancement.ts
@@ -10,6 +10,7 @@ import type {
} from '../../types';
import { withDefaultAuth } from './default-auth';
import { withDelegate } from './delegate';
+import { withJsonProcessor } from './json-processor';
import { Logger } from './logger';
import { withOmit } from './omit';
import { withPassword } from './password';
@@ -90,10 +91,18 @@ export function createEnhancement(
// TODO: move the detection logic into each enhancement
// TODO: how to properly cache the detection result?
+
const allFields = Object.values(options.modelMeta.models).flatMap((modelInfo) => Object.values(modelInfo.fields));
+ if (options.modelMeta.typeDefs) {
+ allFields.push(
+ ...Object.values(options.modelMeta.typeDefs).flatMap((typeDefInfo) => Object.values(typeDefInfo.fields))
+ );
+ }
+
const hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password'));
const hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit'));
const hasDefaultAuth = allFields.some((field) => field.defaultValueProvider);
+ const hasTypeDefField = allFields.some((field) => field.isTypeDef);
const kinds = options.kinds ?? ALL_ENHANCEMENTS;
let result = prisma;
@@ -142,5 +151,9 @@ export function createEnhancement(
result = withOmit(result, options);
}
+ if (hasTypeDefField) {
+ result = withJsonProcessor(result, options);
+ }
+
return result;
}
diff --git a/packages/runtime/src/enhancements/node/default-auth.ts b/packages/runtime/src/enhancements/node/default-auth.ts
index 3852069c8..03ce3750c 100644
--- a/packages/runtime/src/enhancements/node/default-auth.ts
+++ b/packages/runtime/src/enhancements/node/default-auth.ts
@@ -8,6 +8,7 @@ import {
clone,
enumerate,
getFields,
+ getTypeDefInfo,
requireField,
} from '../../cross';
import { DbClientContract, EnhancementContext } from '../../types';
@@ -70,6 +71,11 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
const processCreatePayload = (model: string, data: any) => {
const fields = getFields(this.options.modelMeta, model);
for (const fieldInfo of Object.values(fields)) {
+ if (fieldInfo.isTypeDef) {
+ this.setDefaultValueForTypeDefData(fieldInfo.type, data[fieldInfo.name]);
+ continue;
+ }
+
if (fieldInfo.name in data) {
// create payload already sets field value
continue;
@@ -80,10 +86,10 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
continue;
}
- const authDefaultValue = this.getDefaultValueFromAuth(fieldInfo);
- if (authDefaultValue !== undefined) {
+ const defaultValue = this.getDefaultValue(fieldInfo);
+ if (defaultValue !== undefined) {
// set field value extracted from `auth()`
- this.setAuthDefaultValue(fieldInfo, model, data, authDefaultValue);
+ this.setDefaultValueForModelData(fieldInfo, model, data, defaultValue);
}
}
};
@@ -109,7 +115,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
return newArgs;
}
- private setAuthDefaultValue(fieldInfo: FieldInfo, model: string, data: any, authDefaultValue: unknown) {
+ private setDefaultValueForModelData(fieldInfo: FieldInfo, model: string, data: any, authDefaultValue: unknown) {
if (fieldInfo.isForeignKey && fieldInfo.relationField && fieldInfo.relationField in data) {
// if the field is a fk, and the relation field is already set, we should not override it
return;
@@ -155,7 +161,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
return entry?.[0];
}
- private getDefaultValueFromAuth(fieldInfo: FieldInfo) {
+ private getDefaultValue(fieldInfo: FieldInfo) {
if (!this.userContext) {
throw prismaClientValidationError(
this.prisma,
@@ -165,4 +171,34 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
}
return fieldInfo.defaultValueProvider?.(this.userContext);
}
+
+ private setDefaultValueForTypeDefData(type: string, data: any) {
+ if (!data || (typeof data !== 'object' && !Array.isArray(data))) {
+ return;
+ }
+
+ const typeDef = getTypeDefInfo(this.options.modelMeta, type);
+ if (!typeDef) {
+ return;
+ }
+
+ enumerate(data).forEach((item) => {
+ if (!item || typeof item !== 'object') {
+ return;
+ }
+
+ for (const fieldInfo of Object.values(typeDef.fields)) {
+ if (fieldInfo.isTypeDef) {
+ // recurse
+ this.setDefaultValueForTypeDefData(fieldInfo.type, item[fieldInfo.name]);
+ } else if (!(fieldInfo.name in item)) {
+ // set default value if the payload doesn't set the field
+ const defaultValue = this.getDefaultValue(fieldInfo);
+ if (defaultValue !== undefined) {
+ item[fieldInfo.name] = defaultValue;
+ }
+ }
+ }
+ });
+ }
}
diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts
index 8a7d613a1..78523b837 100644
--- a/packages/runtime/src/enhancements/node/delegate.ts
+++ b/packages/runtime/src/enhancements/node/delegate.ts
@@ -93,7 +93,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
if (args.orderBy) {
// `orderBy` may contain fields from base types
- this.injectWhereHierarchy(this.model, args.orderBy);
+ enumerate(args.orderBy).forEach((item) => this.injectWhereHierarchy(model, item));
}
if (this.options.logPrismaQuery) {
@@ -206,7 +206,9 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
if (fieldValue !== undefined) {
if (fieldValue.orderBy) {
// `orderBy` may contain fields from base types
- this.injectWhereHierarchy(fieldInfo.type, fieldValue.orderBy);
+ enumerate(fieldValue.orderBy).forEach((item) =>
+ this.injectWhereHierarchy(fieldInfo.type, item)
+ );
}
if (this.injectBaseFieldSelect(model, field, fieldValue, args, kind)) {
@@ -1037,7 +1039,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}
if (args.orderBy) {
- this.injectWhereHierarchy(this.model, args.orderBy);
+ enumerate(args.orderBy).forEach((item) => this.injectWhereHierarchy(this.model, item));
}
if (args.where) {
diff --git a/packages/runtime/src/enhancements/node/json-processor.ts b/packages/runtime/src/enhancements/node/json-processor.ts
new file mode 100644
index 000000000..6cf204a6f
--- /dev/null
+++ b/packages/runtime/src/enhancements/node/json-processor.ts
@@ -0,0 +1,92 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { enumerate, getModelFields, resolveField } from '../../cross';
+import { DbClientContract } from '../../types';
+import { InternalEnhancementOptions } from './create-enhancement';
+import { DefaultPrismaProxyHandler, makeProxy, PrismaProxyActions } from './proxy';
+import { QueryUtils } from './query-utils';
+
+/**
+ * Gets an enhanced Prisma client that post-processes JSON values.
+ *
+ * @private
+ */
+export function withJsonProcessor(
+ prisma: DbClient,
+ options: InternalEnhancementOptions
+): DbClient {
+ return makeProxy(
+ prisma,
+ options.modelMeta,
+ (_prisma, model) => new JsonProcessorHandler(_prisma as DbClientContract, model, options),
+ 'json-processor'
+ );
+}
+
+class JsonProcessorHandler extends DefaultPrismaProxyHandler {
+ private queryUtils: QueryUtils;
+
+ constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
+ super(prisma, model, options);
+ this.queryUtils = new QueryUtils(prisma, options);
+ }
+
+ protected override async processResultEntity(_method: PrismaProxyActions, data: T): Promise {
+ for (const value of enumerate(data)) {
+ await this.doPostProcess(value, this.model);
+ }
+ return data;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private async doPostProcess(entityData: any, model: string) {
+ const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);
+
+ for (const field of getModelFields(entityData)) {
+ const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);
+ if (!fieldInfo) {
+ continue;
+ }
+
+ if (fieldInfo.isTypeDef) {
+ this.fixJsonDateFields(entityData[field], fieldInfo.type);
+ } else if (fieldInfo.isDataModel) {
+ const items =
+ fieldInfo.isArray && Array.isArray(entityData[field]) ? entityData[field] : [entityData[field]];
+ for (const item of items) {
+ // recurse
+ await this.doPostProcess(item, fieldInfo.type);
+ }
+ }
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private fixJsonDateFields(entityData: any, typeDef: string) {
+ if (typeof entityData !== 'object' && !Array.isArray(entityData)) {
+ return;
+ }
+
+ enumerate(entityData).forEach((item) => {
+ if (!item || typeof item !== 'object') {
+ return;
+ }
+
+ for (const [key, value] of Object.entries(item)) {
+ const fieldInfo = resolveField(this.options.modelMeta, typeDef, key, true);
+ if (!fieldInfo) {
+ continue;
+ }
+ if (fieldInfo.isTypeDef) {
+ // recurse
+ this.fixJsonDateFields(value, fieldInfo.type);
+ } else if (fieldInfo.type === 'DateTime' && typeof value === 'string') {
+ // convert to Date
+ const parsed = Date.parse(value);
+ if (!isNaN(parsed)) {
+ item[key] = new Date(parsed);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/packages/runtime/src/enhancements/node/policy/policy-utils.ts b/packages/runtime/src/enhancements/node/policy/policy-utils.ts
index ec8a2cfc8..b39ac5b00 100644
--- a/packages/runtime/src/enhancements/node/policy/policy-utils.ts
+++ b/packages/runtime/src/enhancements/node/policy/policy-utils.ts
@@ -3,6 +3,7 @@
import deepmerge from 'deepmerge';
import { isPlainObject } from 'is-plain-object';
import { lowerCaseFirst } from 'lower-case-first';
+import traverse from 'traverse';
import { upperCaseFirst } from 'upper-case-first';
import { z, type ZodError, type ZodObject, type ZodSchema } from 'zod';
import { fromZodError } from 'zod-validation-error';
@@ -31,7 +32,15 @@ import { getVersion } from '../../../version';
import type { InternalEnhancementOptions } from '../create-enhancement';
import { Logger } from '../logger';
import { QueryUtils } from '../query-utils';
-import type { EntityChecker, ModelPolicyDef, PermissionCheckerFunc, PolicyDef, PolicyFunc } from '../types';
+import type {
+ DelegateConstraint,
+ EntityChecker,
+ ModelPolicyDef,
+ PermissionCheckerFunc,
+ PolicyDef,
+ PolicyFunc,
+ VariableConstraint,
+} from '../types';
import { formatObject, prismaClientKnownRequestError } from '../utils';
/**
@@ -667,7 +676,47 @@ export class PolicyUtil extends QueryUtils {
}
// call checker function
- return checker({ user: this.user });
+ let result = checker({ user: this.user });
+
+ // the constraint may contain "delegate" ones that should be resolved
+ // by evaluating the corresponding checker of the delegated models
+
+ const isVariableConstraint = (value: any): value is VariableConstraint => {
+ return value && typeof value === 'object' && value.kind === 'variable';
+ };
+
+ const isDelegateConstraint = (value: any): value is DelegateConstraint => {
+ return value && typeof value === 'object' && value.kind === 'delegate';
+ };
+
+ // here we prefix the constraint variables coming from delegated checkers
+ // with the relation field name to avoid conflicts
+ const prefixConstraintVariables = (constraint: unknown, prefix: string) => {
+ return traverse(constraint).map(function (value) {
+ if (isVariableConstraint(value)) {
+ this.update(
+ {
+ ...value,
+ name: `${prefix}${value.name}`,
+ },
+ true
+ );
+ }
+ });
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const that = this;
+ result = traverse(result).forEach(function (value) {
+ if (isDelegateConstraint(value)) {
+ const { model: delegateModel, relation, operation: delegateOp } = value;
+ let newValue = that.getCheckerConstraint(delegateModel, delegateOp ?? operation);
+ newValue = prefixConstraintVariables(newValue, `${relation}.`);
+ this.update(newValue, true);
+ }
+ });
+
+ return result;
}
//#endregion
diff --git a/packages/runtime/src/enhancements/node/types.ts b/packages/runtime/src/enhancements/node/types.ts
index 37a304b99..c9a90baa8 100644
--- a/packages/runtime/src/enhancements/node/types.ts
+++ b/packages/runtime/src/enhancements/node/types.ts
@@ -18,6 +18,11 @@ export interface CommonEnhancementOptions {
prismaModule?: any;
}
+/**
+ * CRUD operations
+ */
+export type CRUD = 'create' | 'read' | 'update' | 'delete';
+
/**
* Function for getting policy guard with a given context
*/
@@ -74,6 +79,17 @@ export type LogicalConstraint = {
children: PermissionCheckerConstraint[];
};
+/**
+ * Constraint delegated to another model through `check()` function call
+ * on a relation field.
+ */
+export type DelegateConstraint = {
+ kind: 'delegate';
+ model: string;
+ relation: string;
+ operation?: CRUD;
+};
+
/**
* Operation allowability checking constraint
*/
@@ -81,7 +97,8 @@ export type PermissionCheckerConstraint =
| ValueConstraint
| VariableConstraint
| ComparisonConstraint
- | LogicalConstraint;
+ | LogicalConstraint
+ | DelegateConstraint;
/**
* Policy definition
diff --git a/packages/schema/package.json b/packages/schema/package.json
index dcff63940..a99ebeee7 100644
--- a/packages/schema/package.json
+++ b/packages/schema/package.json
@@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack Language Tools",
"description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI",
- "version": "2.7.5",
+ "version": "2.8.0",
"author": {
"name": "ZenStack Team"
},
@@ -123,10 +123,10 @@
"zod-validation-error": "^1.5.0"
},
"peerDependencies": {
- "prisma": "5.0.0 - 5.21.x"
+ "prisma": "5.0.0 - 5.22.x"
},
"devDependencies": {
- "@prisma/client": "5.21.x",
+ "@prisma/client": "5.22.x",
"@types/async-exit-hook": "^2.0.0",
"@types/pluralize": "^0.0.29",
"@types/semver": "^7.3.13",
diff --git a/packages/schema/src/cli/actions/repl.ts b/packages/schema/src/cli/actions/repl.ts
index df15e30fb..fa291a3e2 100644
--- a/packages/schema/src/cli/actions/repl.ts
+++ b/packages/schema/src/cli/actions/repl.ts
@@ -9,13 +9,18 @@ import { inspect } from 'util';
/**
* CLI action for starting a REPL session
*/
-export async function repl(projectPath: string, options: { prismaClient?: string; debug?: boolean; table?: boolean }) {
+export async function repl(
+ projectPath: string,
+ options: { loadPath?: string; prismaClient?: string; debug?: boolean; table?: boolean }
+) {
if (!process?.stdout?.isTTY && process?.versions?.bun) {
- console.error('REPL on Bun is only available in a TTY terminal at this time. Please use npm/npx to run the command in this context instead of bun/bunx.');
+ console.error(
+ 'REPL on Bun is only available in a TTY terminal at this time. Please use npm/npx to run the command in this context instead of bun/bunx.'
+ );
return;
}
- const prettyRepl = await import('pretty-repl')
+ const prettyRepl = await import('pretty-repl');
console.log('Welcome to ZenStack REPL. See help with the ".help" command.');
console.log('Global variables:');
@@ -47,7 +52,9 @@ export async function repl(projectPath: string, options: { prismaClient?: string
}
}
- const { enhance } = require('@zenstackhq/runtime');
+ const { enhance } = options.loadPath
+ ? require(path.join(path.resolve(options.loadPath), 'enhance'))
+ : require('@zenstackhq/runtime');
let debug = !!options.debug;
let table = !!options.table;
@@ -63,7 +70,11 @@ export async function repl(projectPath: string, options: { prismaClient?: string
let r: any = undefined;
let isPrismaCall = false;
- if (cmd.includes('await ')) {
+ if (/^\s*user\s*=[^=]/.test(cmd)) {
+ // assigning to user variable, reset auth
+ eval(cmd);
+ setAuth(user);
+ } else if (/^\s*await\s+/.test(cmd)) {
// eval can't handle top-level await, so we wrap it in an async function
cmd = `(async () => (${cmd}))()`;
r = eval(cmd);
@@ -137,7 +148,7 @@ export async function repl(projectPath: string, options: { prismaClient?: string
// .auth command
replServer.defineCommand('auth', {
- help: 'Set current user. Run without argument to switch to anonymous. Pass an user object to set current user.',
+ help: 'Set current user. Run without argument to switch to anonymous. Pass an user object to set current user. Run ".auth info" to show current user.',
action(value: string) {
this.clearBufferedCommand();
try {
@@ -145,6 +156,10 @@ export async function repl(projectPath: string, options: { prismaClient?: string
// set anonymous
setAuth(undefined);
console.log(`Auth user: anonymous. Use ".auth { id: ... }" to change.`);
+ } else if (value.trim() === 'info') {
+ // refresh auth user
+ setAuth(user);
+ console.log(`Current user: ${user ? inspect(user) : 'anonymous'}`);
} else {
// set current user
const user = eval(`(${value})`);
diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts
index e8773fddd..c58db8c43 100644
--- a/packages/schema/src/cli/index.ts
+++ b/packages/schema/src/cli/index.ts
@@ -133,7 +133,8 @@ export function createProgram() {
program
.command('repl')
.description('Start a REPL session.')
- .option('--prisma-client ', 'path to Prisma client module')
+ .option('--load-path ', 'path to load modules generated by ZenStack')
+ .option('--prisma-client ', 'path to Prisma client module')
.option('--debug', 'enable debug output')
.option('--table', 'enable table format output')
.action(replAction);
diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts
index 2912bfb60..4158bd256 100644
--- a/packages/schema/src/cli/plugin-runner.ts
+++ b/packages/schema/src/cli/plugin-runner.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-var-requires */
-import { isPlugin, Model, Plugin } from '@zenstackhq/language/ast';
+import { DataModel, isPlugin, isTypeDef, Model, Plugin } from '@zenstackhq/language/ast';
import {
createProject,
emitProject,
@@ -311,7 +311,11 @@ export class PluginRunner {
}
private hasValidation(schema: Model) {
- return getDataModels(schema).some((model) => hasValidationAttributes(model));
+ return getDataModels(schema).some((model) => hasValidationAttributes(model) || this.hasTypeDefFields(model));
+ }
+
+ private hasTypeDefFields(model: DataModel) {
+ return model.fields.some((f) => isTypeDef(f.type.reference?.ref));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts
index 9ec2074be..2a7f43c29 100644
--- a/packages/schema/src/language-server/validator/attribute-application-validator.ts
+++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts
@@ -14,8 +14,11 @@ import {
isDataModelField,
isEnum,
isReferenceExpr,
+ isTypeDef,
+ isTypeDefField,
} from '@zenstackhq/language/ast';
import {
+ hasAttribute,
isDataModelFieldReference,
isDelegateModel,
isFutureExpr,
@@ -62,6 +65,10 @@ export default class AttributeApplicationValidator implements AstValidator();
for (const arg of attr.args) {
@@ -345,6 +352,9 @@ function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataModelField)
case 'ModelField':
allowed = allowed || isDataModel(targetDecl.type.reference?.ref);
break;
+ case 'TypeDefField':
+ allowed = allowed || isTypeDef(targetDecl.type.reference?.ref);
+ break;
default:
break;
}
diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts
index e463f740f..f2a3d6737 100644
--- a/packages/schema/src/language-server/validator/datamodel-validator.ts
+++ b/packages/schema/src/language-server/validator/datamodel-validator.ts
@@ -6,8 +6,16 @@ import {
isDataModel,
isEnum,
isStringLiteral,
+ isTypeDef,
} from '@zenstackhq/language/ast';
-import { getModelFieldsWithBases, getModelIdFields, getModelUniqueFields, isDelegateModel } from '@zenstackhq/sdk';
+import {
+ getDataSourceProvider,
+ getModelFieldsWithBases,
+ getModelIdFields,
+ getModelUniqueFields,
+ hasAttribute,
+ isDelegateModel,
+} from '@zenstackhq/sdk';
import { AstNode, DiagnosticInfo, ValidationAcceptor, getDocument } from 'langium';
import { findUpInheritance } from '../../utils/ast-utils';
import { IssueCodes, SCALAR_TYPES } from '../constants';
@@ -95,6 +103,16 @@ export default class DataModelValidator implements AstValidator {
}
field.attributes.forEach((attr) => validateAttributeApplication(attr, accept));
+
+ if (isTypeDef(field.type.reference?.ref)) {
+ if (!hasAttribute(field, '@json')) {
+ accept('error', 'Custom-typed field must have @json attribute', { node: field });
+ }
+
+ if (getDataSourceProvider(field.$container.$container) !== 'postgresql') {
+ accept('error', 'Custom-typed field is only supported with "postgresql" provider', { node: field });
+ }
+ }
}
private validateAttributes(dm: DataModel, accept: ValidationAcceptor) {
diff --git a/packages/schema/src/language-server/validator/typedef-validator.ts b/packages/schema/src/language-server/validator/typedef-validator.ts
new file mode 100644
index 000000000..55c127d7d
--- /dev/null
+++ b/packages/schema/src/language-server/validator/typedef-validator.ts
@@ -0,0 +1,23 @@
+import { TypeDef, TypeDefField } from '@zenstackhq/language/ast';
+import { ValidationAcceptor } from 'langium';
+import { AstValidator } from '../types';
+import { validateAttributeApplication } from './attribute-application-validator';
+import { validateDuplicatedDeclarations } from './utils';
+
+/**
+ * Validates type def declarations.
+ */
+export default class TypeDefValidator implements AstValidator {
+ validate(typeDef: TypeDef, accept: ValidationAcceptor): void {
+ validateDuplicatedDeclarations(typeDef, typeDef.fields, accept);
+ this.validateFields(typeDef, accept);
+ }
+
+ private validateFields(typeDef: TypeDef, accept: ValidationAcceptor) {
+ typeDef.fields.forEach((field) => this.validateField(field, accept));
+ }
+
+ private validateField(field: TypeDefField, accept: ValidationAcceptor): void {
+ field.attributes.forEach((attr) => validateAttributeApplication(attr, accept));
+ }
+}
diff --git a/packages/schema/src/language-server/validator/zmodel-validator.ts b/packages/schema/src/language-server/validator/zmodel-validator.ts
index 493bc5f89..c1dcbb09e 100644
--- a/packages/schema/src/language-server/validator/zmodel-validator.ts
+++ b/packages/schema/src/language-server/validator/zmodel-validator.ts
@@ -7,6 +7,7 @@ import {
FunctionDecl,
InvocationExpr,
Model,
+ TypeDef,
ZModelAstType,
} from '@zenstackhq/language/ast';
import { AstNode, LangiumDocument, ValidationAcceptor, ValidationChecks, ValidationRegistry } from 'langium';
@@ -19,6 +20,7 @@ import ExpressionValidator from './expression-validator';
import FunctionDeclValidator from './function-decl-validator';
import FunctionInvocationValidator from './function-invocation-validator';
import SchemaValidator from './schema-validator';
+import TypeDefValidator from './typedef-validator';
/**
* Registry for validation checks.
@@ -31,6 +33,7 @@ export class ZModelValidationRegistry extends ValidationRegistry {
Model: validator.checkModel,
DataSource: validator.checkDataSource,
DataModel: validator.checkDataModel,
+ TypeDef: validator.checkTypeDef,
Enum: validator.checkEnum,
Attribute: validator.checkAttribute,
Expression: validator.checkExpression,
@@ -73,6 +76,10 @@ export class ZModelValidator {
this.shouldCheck(node) && new DataModelValidator().validate(node, accept);
}
+ checkTypeDef(node: TypeDef, accept: ValidationAcceptor): void {
+ this.shouldCheck(node) && new TypeDefValidator().validate(node, accept);
+ }
+
checkEnum(node: Enum, accept: ValidationAcceptor): void {
this.shouldCheck(node) && new EnumValidator().validate(node, accept);
}
diff --git a/packages/schema/src/language-server/zmodel-documentation-provider.ts b/packages/schema/src/language-server/zmodel-documentation-provider.ts
new file mode 100644
index 000000000..f960507bc
--- /dev/null
+++ b/packages/schema/src/language-server/zmodel-documentation-provider.ts
@@ -0,0 +1,16 @@
+import { AstNode, JSDocDocumentationProvider } from 'langium';
+
+/**
+ * Documentation provider that first tries to use triple-slash comments and falls back to JSDoc comments.
+ */
+export class ZModelDocumentationProvider extends JSDocDocumentationProvider {
+ getDocumentation(node: AstNode): string | undefined {
+ // prefer to use triple-slash comments
+ if ('comments' in node && Array.isArray(node.comments) && node.comments.length > 0) {
+ return node.comments.map((c: string) => c.replace(/^[/]*\s*/, '')).join('\n');
+ }
+
+ // fall back to JSDoc comments
+ return super.getDocumentation(node);
+ }
+}
diff --git a/packages/schema/src/language-server/zmodel-module.ts b/packages/schema/src/language-server/zmodel-module.ts
index 116d486da..701d31d87 100644
--- a/packages/schema/src/language-server/zmodel-module.ts
+++ b/packages/schema/src/language-server/zmodel-module.ts
@@ -27,13 +27,14 @@ import { ZModelValidationRegistry, ZModelValidator } from './validator/zmodel-va
import { ZModelCodeActionProvider } from './zmodel-code-action';
import { ZModelCompletionProvider } from './zmodel-completion-provider';
import { ZModelDefinitionProvider } from './zmodel-definition';
+import { ZModelDocumentationProvider } from './zmodel-documentation-provider';
import { ZModelFormatter } from './zmodel-formatter';
import { ZModelHighlightProvider } from './zmodel-highlight';
import { ZModelHoverProvider } from './zmodel-hover';
import { ZModelLinker } from './zmodel-linker';
import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope';
import { ZModelSemanticTokenProvider } from './zmodel-semantic';
-import ZModelWorkspaceManager from './zmodel-workspace-manager';
+import { ZModelWorkspaceManager } from './zmodel-workspace-manager';
/**
* Declaration of custom services - add your own service classes here.
@@ -77,6 +78,9 @@ export const ZModelModule: Module createGrammarConfig(services),
},
+ documentation: {
+ DocumentationProvider: (services) => new ZModelDocumentationProvider(services),
+ },
};
// this duplicates createDefaultSharedModule except that a custom WorkspaceManager is used
diff --git a/packages/schema/src/language-server/zmodel-workspace-manager.ts b/packages/schema/src/language-server/zmodel-workspace-manager.ts
index 79b5bfb5e..734a785cd 100644
--- a/packages/schema/src/language-server/zmodel-workspace-manager.ts
+++ b/packages/schema/src/language-server/zmodel-workspace-manager.ts
@@ -9,7 +9,7 @@ import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants';
/**
* Custom Langium WorkspaceManager implementation which automatically loads stdlib.zmodel
*/
-export default class ZModelWorkspaceManager extends DefaultWorkspaceManager {
+export class ZModelWorkspaceManager extends DefaultWorkspaceManager {
public pluginModels = new Set();
protected async loadAdditionalDocuments(
diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts
index 34cf26640..0b69b6a25 100644
--- a/packages/schema/src/plugins/enhancer/enhance/index.ts
+++ b/packages/schema/src/plugins/enhancer/enhance/index.ts
@@ -19,6 +19,7 @@ import {
isDataModel,
isGeneratorDecl,
isReferenceExpr,
+ isTypeDef,
type Model,
} from '@zenstackhq/sdk/ast';
import { getDMMF, getPrismaClientImportSpec, getPrismaVersion, type DMMF } from '@zenstackhq/sdk/prisma';
@@ -45,6 +46,7 @@ import { PrismaSchemaGenerator } from '../../prisma/schema-generator';
import { isDefaultWithAuth } from '../enhancer-utils';
import { generateAuthType } from './auth-type-generator';
import { generateCheckerType } from './checker-type-generator';
+import { generateTypeDefType } from './model-typedef-generator';
// information of delegate models and their sub models
type DelegateInfo = [DataModel, DataModel[]][];
@@ -60,35 +62,27 @@ export class EnhancerGenerator {
) {}
async generate(): Promise<{ dmmf: DMMF.Document | undefined }> {
- let logicalPrismaClientDir: string | undefined;
let dmmf: DMMF.Document | undefined;
const prismaImport = getPrismaClientImportSpec(this.outDir, this.options);
+ let prismaTypesFixed = false;
+ let resultPrismaImport = prismaImport;
- if (this.needsLogicalClient()) {
- // schema contains delegate models, need to generate a logical prisma schema
+ if (this.needsLogicalClient || this.needsPrismaClientTypeFixes) {
+ prismaTypesFixed = true;
+ resultPrismaImport = `${LOGICAL_CLIENT_GENERATION_PATH}/index-fixed`;
const result = await this.generateLogicalPrisma();
-
- logicalPrismaClientDir = LOGICAL_CLIENT_GENERATION_PATH;
dmmf = result.dmmf;
-
- // create a reexport of the logical prisma client
- const prismaDts = this.project.createSourceFile(
- path.join(this.outDir, 'models.d.ts'),
- `export type * from '${logicalPrismaClientDir}/index-fixed';`,
- { overwrite: true }
- );
- await prismaDts.save();
- } else {
- // just reexport the prisma client
- const prismaDts = this.project.createSourceFile(
- path.join(this.outDir, 'models.d.ts'),
- `export type * from '${prismaImport}';`,
- { overwrite: true }
- );
- await prismaDts.save();
}
+ // reexport PrismaClient types (original or fixed)
+ const prismaDts = this.project.createSourceFile(
+ path.join(this.outDir, 'models.d.ts'),
+ `export type * from '${resultPrismaImport}';`,
+ { overwrite: true }
+ );
+ await prismaDts.save();
+
const authModel = getAuthModel(getDataModels(this.model));
const authTypes = authModel ? generateAuthType(this.model, authModel) : '';
const authTypeParam = authModel ? `auth.${authModel.name}` : 'AuthUser';
@@ -112,8 +106,8 @@ ${
}
${
- logicalPrismaClientDir
- ? this.createLogicalPrismaImports(prismaImport, logicalPrismaClientDir)
+ prismaTypesFixed
+ ? this.createLogicalPrismaImports(prismaImport, resultPrismaImport)
: this.createSimplePrismaImports(prismaImport)
}
@@ -122,7 +116,7 @@ ${authTypes}
${checkerTypes}
${
- logicalPrismaClientDir
+ prismaTypesFixed
? this.createLogicalPrismaEnhanceFunction(authTypeParam)
: this.createSimplePrismaEnhanceFunction(authTypeParam)
}
@@ -185,11 +179,11 @@ export function enhance(prisma: DbClient, context?: Enh
`;
}
- private createLogicalPrismaImports(prismaImport: string, logicalPrismaClientDir: string) {
+ private createLogicalPrismaImports(prismaImport: string, prismaClientImport: string) {
return `import { Prisma as _Prisma, PrismaClient as _PrismaClient } from '${prismaImport}';
import type { InternalArgs, DynamicClientExtensionThis } from '${prismaImport}/runtime/library';
-import type * as _P from '${logicalPrismaClientDir}/index-fixed';
-import type { Prisma, PrismaClient } from '${logicalPrismaClientDir}/index-fixed';
+import type * as _P from '${prismaClientImport}';
+import type { Prisma, PrismaClient } from '${prismaClientImport}';
export type { PrismaClient };
`;
}
@@ -229,10 +223,14 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara
`;
}
- private needsLogicalClient() {
+ private get needsLogicalClient() {
return this.hasDelegateModel(this.model) || this.hasAuthInDefault(this.model);
}
+ private get needsPrismaClientTypeFixes() {
+ return this.hasTypeDef(this.model);
+ }
+
private hasDelegateModel(model: Model) {
const dataModels = getDataModels(model);
return dataModels.some(
@@ -246,6 +244,10 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara
);
}
+ private hasTypeDef(model: Model) {
+ return model.declarations.some(isTypeDef);
+ }
+
private async generateLogicalPrisma() {
const prismaGenerator = new PrismaSchemaGenerator(this.model);
@@ -349,18 +351,15 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara
overwrite: true,
});
- if (delegateInfo.length > 0) {
- // transform types for delegated models
- this.transformDelegate(sf, sfNew, delegateInfo);
- sfNew.formatText();
- } else {
- // just copy
- sfNew.replaceWithText(sf.getFullText());
- }
+ this.transformPrismaTypes(sf, sfNew, delegateInfo);
+
+ this.generateExtraTypes(sfNew);
+
+ sfNew.formatText();
await sfNew.save();
}
- private transformDelegate(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) {
+ private transformPrismaTypes(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) {
// copy toplevel imports
sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure()));
@@ -493,10 +492,72 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara
// fix delegate payload union type
source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source);
+ // fix json field type
+ source = this.fixJsonFieldType(typeAlias, source);
+
structure.type = source;
return structure;
}
+ private fixJsonFieldType(typeAlias: TypeAliasDeclaration, source: string) {
+ const modelsWithTypeField = this.model.declarations.filter(
+ (d): d is DataModel => isDataModel(d) && d.fields.some((f) => isTypeDef(f.type.reference?.ref))
+ );
+ const typeName = typeAlias.getName();
+
+ const getTypedJsonFields = (model: DataModel) => {
+ return model.fields.filter((f) => isTypeDef(f.type.reference?.ref));
+ };
+
+ const replacePrismaJson = (source: string, field: DataModelField) => {
+ return source.replace(
+ new RegExp(`(${field.name}\\??\\s*):[^\\n]+`),
+ `$1: ${field.type.reference!.$refText}${field.type.array ? '[]' : ''}${
+ field.type.optional ? ' | null' : ''
+ }`
+ );
+ };
+
+ // fix "$[Model]Payload" type
+ const payloadModelMatch = modelsWithTypeField.find((m) => `$${m.name}Payload` === typeName);
+ if (payloadModelMatch) {
+ const scalars = typeAlias
+ .getDescendantsOfKind(SyntaxKind.PropertySignature)
+ .find((p) => p.getName() === 'scalars');
+ if (!scalars) {
+ return source;
+ }
+
+ const fieldsToFix = getTypedJsonFields(payloadModelMatch);
+ for (const field of fieldsToFix) {
+ source = replacePrismaJson(source, field);
+ }
+ }
+
+ // fix input/output types, "[Model]CreateInput", etc.
+ const inputOutputModelMatch = modelsWithTypeField.find((m) => typeName.startsWith(m.name));
+ if (inputOutputModelMatch) {
+ const relevantTypePatterns = [
+ 'GroupByOutputType',
+ '(Unchecked)?Create(\\S+?)?Input',
+ '(Unchecked)?Update(\\S+?)?Input',
+ 'CreateManyInput',
+ '(Unchecked)?UpdateMany(Mutation)?Input',
+ ];
+ const typeRegex = modelsWithTypeField.map(
+ (m) => new RegExp(`^(${m.name})(${relevantTypePatterns.join('|')})$`)
+ );
+ if (typeRegex.some((r) => r.test(typeName))) {
+ const fieldsToFix = getTypedJsonFields(inputOutputModelMatch);
+ for (const field of fieldsToFix) {
+ source = replacePrismaJson(source, field);
+ }
+ }
+ }
+
+ return source;
+ }
+
private fixDelegatePayloadType(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo, source: string) {
// change the type of `$Payload` type of delegate model to a union of concrete types
const typeName = typeAlias.getName();
@@ -677,4 +738,12 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara
private get generatePermissionChecker() {
return this.options.generatePermissionChecker === true;
}
+
+ private async generateExtraTypes(sf: SourceFile) {
+ for (const decl of this.model.declarations) {
+ if (isTypeDef(decl)) {
+ generateTypeDefType(sf, decl);
+ }
+ }
+ }
}
diff --git a/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts b/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts
new file mode 100644
index 000000000..40bc3e5b4
--- /dev/null
+++ b/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts
@@ -0,0 +1,63 @@
+import { PluginError } from '@zenstackhq/sdk';
+import { BuiltinType, TypeDef, TypeDefFieldType } from '@zenstackhq/sdk/ast';
+import { SourceFile } from 'ts-morph';
+import { match } from 'ts-pattern';
+import { name } from '..';
+
+export function generateTypeDefType(sourceFile: SourceFile, decl: TypeDef) {
+ sourceFile.addTypeAlias({
+ name: decl.name,
+ isExported: true,
+ docs: decl.comments.map((c) => unwrapTripleSlashComment(c)),
+ type: (writer) => {
+ writer.block(() => {
+ decl.fields.forEach((field) => {
+ if (field.comments.length > 0) {
+ writer.writeLine(` /**`);
+ field.comments.forEach((c) => writer.writeLine(` * ${unwrapTripleSlashComment(c)}`));
+ writer.writeLine(` */`);
+ }
+ writer.writeLine(
+ ` ${field.name}${field.type.optional ? '?' : ''}: ${zmodelTypeToTsType(field.type)};`
+ );
+ });
+ });
+ },
+ });
+}
+
+function unwrapTripleSlashComment(c: string): string {
+ return c.replace(/^[/]*\s*/, '');
+}
+
+function zmodelTypeToTsType(type: TypeDefFieldType) {
+ let result: string;
+
+ if (type.type) {
+ result = builtinTypeToTsType(type.type);
+ } else if (type.reference?.ref) {
+ result = type.reference.ref.name;
+ } else {
+ throw new PluginError(name, `Unsupported field type: ${type}`);
+ }
+
+ if (type.array) {
+ result += '[]';
+ }
+
+ return result;
+}
+
+function builtinTypeToTsType(type: BuiltinType) {
+ return match(type)
+ .with('Boolean', () => 'boolean')
+ .with('BigInt', () => 'bigint')
+ .with('Int', () => 'number')
+ .with('Float', () => 'number')
+ .with('Decimal', () => 'Prisma.Decimal')
+ .with('String', () => 'string')
+ .with('Bytes', () => 'Uint8Array')
+ .with('DateTime', () => 'Date')
+ .with('Json', () => 'unknown')
+ .exhaustive();
+}
diff --git a/packages/schema/src/plugins/enhancer/model-meta/index.ts b/packages/schema/src/plugins/enhancer/model-meta/index.ts
index 38757ae6f..53fecdb82 100644
--- a/packages/schema/src/plugins/enhancer/model-meta/index.ts
+++ b/packages/schema/src/plugins/enhancer/model-meta/index.ts
@@ -1,15 +1,16 @@
import { generateModelMeta, getDataModels, type PluginOptions } from '@zenstackhq/sdk';
-import type { Model } from '@zenstackhq/sdk/ast';
+import { isTypeDef, type Model } from '@zenstackhq/sdk/ast';
import path from 'path';
import type { Project } from 'ts-morph';
export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) {
const outFile = path.join(outDir, 'model-meta.ts');
const dataModels = getDataModels(model);
+ const typeDefs = model.declarations.filter(isTypeDef);
// save ts files if requested explicitly or the user provided
const preserveTsFiles = options.preserveTsFiles === true || !!options.output;
- await generateModelMeta(project, dataModels, {
+ await generateModelMeta(project, dataModels, typeDefs, {
output: outFile,
generateAttributes: true,
preserveTsFiles,
diff --git a/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts b/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts
index a0b1c1dd2..674348470 100644
--- a/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts
+++ b/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts
@@ -1,4 +1,6 @@
import {
+ PluginError,
+ getLiteral,
getRelationKeyPairs,
isAuthInvocation,
isDataModelFieldReference,
@@ -7,9 +9,11 @@ import {
import {
BinaryExpr,
BooleanLiteral,
+ DataModel,
DataModelField,
Expression,
ExpressionType,
+ InvocationExpr,
LiteralExpr,
MemberAccessExpr,
NumberLiteral,
@@ -27,6 +31,8 @@ import {
isUnaryExpr,
} from '@zenstackhq/sdk/ast';
import { P, match } from 'ts-pattern';
+import { name } from '..';
+import { isCheckInvocation } from '../../../utils/ast-utils';
/**
* Options for {@link ConstraintTransformer}.
@@ -107,6 +113,8 @@ export class ConstraintTransformer {
.when(isReferenceExpr, (expr) => this.transformReference(expr))
// top-level boolean member access expr
.when(isMemberAccessExpr, (expr) => this.transformMemberAccess(expr))
+ // `check()` invocation on a relation field
+ .when(isCheckInvocation, (expr) => this.transformCheckInvocation(expr as InvocationExpr))
.otherwise(() => this.nextVar())
);
}
@@ -259,6 +267,30 @@ export class ConstraintTransformer {
return undefined;
}
+ private transformCheckInvocation(expr: InvocationExpr) {
+ // transform `check()` invocation to a special "delegate" constraint kind
+ // to be evaluated at runtime
+
+ const field = expr.args[0].value as ReferenceExpr;
+ if (!field) {
+ throw new PluginError(name, 'Invalid check invocation');
+ }
+ const fieldType = field.$resolvedType?.decl as DataModel;
+
+ let operation: string | undefined = undefined;
+ if (expr.args[1]) {
+ operation = getLiteral(expr.args[1].value);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const result: any = { kind: 'delegate', model: fieldType.name, relation: field.target.$refText };
+ if (operation) {
+ // operation can be explicitly specified or inferred from the context
+ result.operation = operation;
+ }
+ return JSON.stringify(result);
+ }
+
// normalize `auth()` access undefined value to null
private normalizeToNull(expr: string) {
return `(${expr} ?? null)`;
diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts
index aa54c80d8..62469f744 100644
--- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts
+++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts
@@ -11,6 +11,7 @@ import {
isMemberAccessExpr,
isReferenceExpr,
isThisExpr,
+ isTypeDef,
} from '@zenstackhq/language/ast';
import { PolicyCrudKind, type PolicyOperationKind } from '@zenstackhq/runtime';
import {
@@ -747,7 +748,14 @@ export class PolicyGenerator {
for (const model of models) {
writer.write(`${lowerCaseFirst(model.name)}:`);
writer.inlineBlock(() => {
- writer.write(`hasValidation: ${hasValidationAttributes(model)}`);
+ writer.write(
+ `hasValidation: ${
+ // explicit validation rules
+ hasValidationAttributes(model) ||
+ // type-def fields require schema validation
+ this.hasTypeDefFields(model)
+ }`
+ );
});
writer.writeLine(',');
}
@@ -755,5 +763,9 @@ export class PolicyGenerator {
writer.writeLine(',');
}
+ private hasTypeDefFields(model: DataModel): boolean {
+ return model.fields.some((f) => isTypeDef(f.type.reference?.ref));
+ }
+
// #endregion
}
diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts
index 54d111d88..9a787ab8b 100644
--- a/packages/schema/src/plugins/prisma/schema-generator.ts
+++ b/packages/schema/src/plugins/prisma/schema-generator.ts
@@ -1,5 +1,4 @@
import {
- AbstractDeclaration,
AttributeArg,
BooleanLiteral,
ConfigArrayExpr,
@@ -24,6 +23,7 @@ import {
isNullExpr,
isReferenceExpr,
isStringLiteral,
+ isTypeDef,
LiteralExpr,
Model,
NumberLiteral,
@@ -100,6 +100,7 @@ export class PrismaSchemaGenerator {
`;
private mode: 'logical' | 'physical' = 'physical';
+ private customAttributesAsComments = false;
// a mapping from full names to shortened names
private shortNameMap = new Map();
@@ -117,6 +118,14 @@ export class PrismaSchemaGenerator {
this.mode = options.mode as 'logical' | 'physical';
}
+ if (
+ options.customAttributesAsComments !== undefined &&
+ typeof options.customAttributesAsComments !== 'boolean'
+ ) {
+ throw new PluginError(name, 'option "customAttributesAsComments" must be a boolean');
+ }
+ this.customAttributesAsComments = options.customAttributesAsComments === true;
+
const prismaVersion = getPrismaVersion();
if (prismaVersion && semver.lt(prismaVersion, PRISMA_MINIMUM_VERSION)) {
warnings.push(
@@ -282,24 +291,21 @@ export class PrismaSchemaGenerator {
this.generateContainerAttribute(model, attr);
}
- decl.attributes
- .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr))
- .forEach((attr) => model.addComment('/// ' + this.zModelGenerator.generate(attr)));
-
// user defined comments pass-through
decl.comments.forEach((c) => model.addComment(c));
+ this.getCustomAttributesAsComments(decl).forEach((c) => model.addComment(c));
- // generate relation fields on base models linking to concrete models
+ // physical: generate relation fields on base models linking to concrete models
this.generateDelegateRelationForBase(model, decl);
- // generate reverse relation fields on concrete models
+ // physical: generate reverse relation fields on concrete models
this.generateDelegateRelationForConcrete(model, decl);
- // expand relations on other models that reference delegated models to concrete models
+ // logical: expand relations on other models that reference delegated models to concrete models
this.expandPolymorphicRelations(model, decl);
- // name relations inherited from delegate base models for disambiguation
- this.nameRelationsInheritedFromDelegate(model, decl);
+ // logical: ensure relations inherited from delegate models
+ this.ensureRelationsInheritedFromDelegate(model, decl);
}
private generateDelegateRelationForBase(model: PrismaDataModel, decl: DataModel) {
@@ -397,7 +403,7 @@ export class PrismaSchemaGenerator {
// find concrete models that inherit from this field's model type
const concreteModels = dataModel.$container.declarations.filter(
- (d) => isDataModel(d) && isDescendantOf(d, fieldType)
+ (d): d is DataModel => isDataModel(d) && isDescendantOf(d, fieldType)
);
concreteModels.forEach((concrete) => {
@@ -412,10 +418,9 @@ export class PrismaSchemaGenerator {
);
const relAttr = getAttribute(field, '@relation');
+ let relAttrAdded = false;
if (relAttr) {
- const fieldsArg = getAttributeArg(relAttr, 'fields');
- const nameArg = getAttributeArg(relAttr, 'name') as LiteralExpr;
- if (fieldsArg) {
+ if (getAttributeArg(relAttr, 'fields')) {
// for reach foreign key field pointing to the delegate model, we need to create an aux foreign key
// to point to the concrete model
const relationFieldPairs = getRelationKeyPairs(field);
@@ -444,10 +449,7 @@ export class PrismaSchemaGenerator {
const addedRel = new PrismaFieldAttribute('@relation', [
// use field name as relation name for disambiguation
- new PrismaAttributeArg(
- undefined,
- new AttributeArgValue('String', nameArg?.value || auxRelationField.name)
- ),
+ new PrismaAttributeArg(undefined, new AttributeArgValue('String', auxRelationField.name)),
new PrismaAttributeArg('fields', fieldsArg),
new PrismaAttributeArg('references', referencesArg),
]);
@@ -461,12 +463,12 @@ export class PrismaSchemaGenerator {
)
);
}
-
auxRelationField.attributes.push(addedRel);
- } else {
- auxRelationField.attributes.push(this.makeFieldAttribute(relAttr as DataModelFieldAttribute));
+ relAttrAdded = true;
}
- } else {
+ }
+
+ if (!relAttrAdded) {
auxRelationField.attributes.push(
new PrismaFieldAttribute('@relation', [
// use field name as relation name for disambiguation
@@ -480,8 +482,8 @@ export class PrismaSchemaGenerator {
private replicateForeignKey(
model: PrismaDataModel,
- dataModel: DataModel,
- concreteModel: AbstractDeclaration,
+ delegateModel: DataModel,
+ concreteModel: DataModel,
origForeignKey: DataModelField
) {
// aux fk name format: delegate_aux_[model]_[fkField]_[concrete]
@@ -493,26 +495,20 @@ export class PrismaSchemaGenerator {
// `@map` attribute should not be inherited
addedFkField.attributes = addedFkField.attributes.filter((attr) => !('name' in attr && attr.name === '@map'));
+ // `@unique` attribute should be recreated with disambiguated name
+ addedFkField.attributes = addedFkField.attributes.filter(
+ (attr) => !('name' in attr && attr.name === '@unique')
+ );
+ const uniqueAttr = addedFkField.addAttribute('@unique');
+ const constraintName = this.truncate(`${delegateModel.name}_${addedFkField.name}_${concreteModel.name}_unique`);
+ uniqueAttr.args.push(new PrismaAttributeArg('map', new AttributeArgValue('String', constraintName)));
+
// fix its name
- const addedFkFieldName = `${dataModel.name}_${origForeignKey.name}_${concreteModel.name}`;
+ const addedFkFieldName = `${delegateModel.name}_${origForeignKey.name}_${concreteModel.name}`;
addedFkField.name = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${addedFkFieldName}`);
- // we also need to make sure `@unique` constraint's `map` parameter is fixed to avoid conflict
- const uniqueAttr = addedFkField.attributes.find(
- (attr) => (attr as PrismaFieldAttribute).name === '@unique'
- ) as PrismaFieldAttribute;
- if (uniqueAttr) {
- const mapArg = uniqueAttr.args.find((arg) => arg.name === 'map');
- const constraintName = this.truncate(`${addedFkField.name}_unique`);
- if (mapArg) {
- mapArg.value = new AttributeArgValue('String', constraintName);
- } else {
- uniqueAttr.args.push(new PrismaAttributeArg('map', new AttributeArgValue('String', constraintName)));
- }
- }
-
// we also need to go through model-level `@@unique` and replicate those involving fk fields
- this.replicateForeignKeyModelLevelUnique(model, dataModel, origForeignKey, addedFkField);
+ this.replicateForeignKeyModelLevelUnique(model, delegateModel, origForeignKey, addedFkField);
return addedFkField;
}
@@ -590,13 +586,11 @@ export class PrismaSchemaGenerator {
return shortName;
}
- private nameRelationsInheritedFromDelegate(model: PrismaDataModel, decl: DataModel) {
+ private ensureRelationsInheritedFromDelegate(model: PrismaDataModel, decl: DataModel) {
if (this.mode !== 'logical') {
return;
}
- // the logical schema needs to name relations inherited from delegate base models for disambiguation
-
decl.fields.forEach((f) => {
if (!isDataModel(f.type.reference?.ref)) {
// only process relation fields
@@ -630,30 +624,68 @@ export class PrismaSchemaGenerator {
if (!oppositeRelationField) {
return;
}
+ const oppositeRelationAttr = getAttribute(oppositeRelationField, '@relation');
const fieldType = f.type.reference.ref;
// relation name format: delegate_aux_[relationType]_[oppositeRelationField]_[concrete]
- const relAttr = getAttribute(f, '@relation');
- const name = `${fieldType.name}_${oppositeRelationField.name}_${decl.name}`;
- const relName = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${name}`);
-
- if (relAttr) {
- const nameArg = getAttributeArg(relAttr, 'name');
- if (!nameArg) {
- const prismaRelAttr = prismaField.attributes.find(
- (attr) => (attr as PrismaFieldAttribute).name === '@relation'
- ) as PrismaFieldAttribute;
- if (prismaRelAttr) {
- prismaRelAttr.args.unshift(
- new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName))
- );
- }
- }
+ const relName = this.truncate(
+ `${DELEGATE_AUX_RELATION_PREFIX}_${fieldType.name}_${oppositeRelationField.name}_${decl.name}`
+ );
+
+ // recreate `@relation` attribute
+ prismaField.attributes = prismaField.attributes.filter(
+ (attr) => (attr as PrismaFieldAttribute).name !== '@relation'
+ );
+
+ if (
+ // array relation doesn't need FK
+ f.type.array ||
+ // opposite relation already has FK, we don't need to generate on this side
+ (oppositeRelationAttr && getAttributeArg(oppositeRelationAttr, 'fields'))
+ ) {
+ prismaField.attributes.push(
+ new PrismaFieldAttribute('@relation', [
+ new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName)),
+ ])
+ );
} else {
+ // generate FK field
+ const oppositeModelIds = getIdFields(oppositeRelationField.$container as DataModel);
+ const fkFieldNames: string[] = [];
+
+ oppositeModelIds.forEach((idField) => {
+ const fkFieldName = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${f.name}_${idField.name}`);
+ model.addField(fkFieldName, new ModelFieldType(idField.type.type!, false, f.type.optional), [
+ // one-to-one relation requires FK field to be unique, we're just including it
+ // in all cases since it doesn't hurt
+ new PrismaFieldAttribute('@unique'),
+ ]);
+ fkFieldNames.push(fkFieldName);
+ });
+
prismaField.attributes.push(
new PrismaFieldAttribute('@relation', [
new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName)),
+ new PrismaAttributeArg(
+ 'fields',
+ new AttributeArgValue(
+ 'Array',
+ fkFieldNames.map(
+ (fk) => new AttributeArgValue('FieldReference', new PrismaFieldReference(fk))
+ )
+ )
+ ),
+ new PrismaAttributeArg(
+ 'references',
+ new AttributeArgValue(
+ 'Array',
+ oppositeModelIds.map(
+ (idField) =>
+ new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name))
+ )
+ )
+ ),
])
);
}
@@ -684,9 +716,24 @@ export class PrismaSchemaGenerator {
private getOppositeRelationField(oppositeModel: DataModel, relationField: DataModelField) {
const relName = this.getRelationName(relationField);
- return oppositeModel.fields.find(
+ const matches = oppositeModel.fields.filter(
(f) => f.type.reference?.ref === relationField.$container && this.getRelationName(f) === relName
);
+
+ if (matches.length === 0) {
+ return undefined;
+ } else if (matches.length === 1) {
+ return matches[0];
+ } else {
+ // if there are multiple matches, prefer to use the one with the same field name,
+ // this can happen with self-relations
+ const withNameMatch = matches.find((f) => f.name === relationField.name);
+ if (withNameMatch) {
+ return withNameMatch;
+ } else {
+ return matches[0];
+ }
+ }
}
private getRelationName(field: DataModelField) {
@@ -739,13 +786,34 @@ export class PrismaSchemaGenerator {
}
private generateModelField(model: PrismaDataModel, field: DataModelField, addToFront = false) {
- const fieldType =
- field.type.type || field.type.reference?.ref?.name || this.getUnsupportedFieldType(field.type);
+ let fieldType: string | undefined;
+
+ if (field.type.type) {
+ // intrinsic type
+ fieldType = field.type.type;
+ } else if (field.type.reference?.ref) {
+ // model, enum, or type-def
+ if (isTypeDef(field.type.reference.ref)) {
+ fieldType = 'Json';
+ } else {
+ fieldType = field.type.reference.ref.name;
+ }
+ } else {
+ // Unsupported type
+ const unsupported = this.getUnsupportedFieldType(field.type);
+ if (unsupported) {
+ fieldType = unsupported;
+ }
+ }
+
if (!fieldType) {
throw new PluginError(name, `Field type is not resolved: ${field.$container.name}.${field.name}`);
}
- const type = new ModelFieldType(fieldType, field.type.array, field.type.optional);
+ const isArray =
+ // typed-JSON fields should be translated to scalar Json type
+ isTypeDef(field.type.reference?.ref) ? false : field.type.array;
+ const type = new ModelFieldType(fieldType, isArray, field.type.optional);
const attributes = field.attributes
.filter((attr) => this.isPrismaAttribute(attr))
@@ -763,11 +831,9 @@ export class PrismaSchemaGenerator {
)
.map((attr) => this.makeFieldAttribute(attr));
- const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr));
-
- const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generate(attr));
-
- const result = model.addField(field.name, type, attributes, documentations, addToFront);
+ // user defined comments pass-through
+ const docs = [...field.comments, ...this.getCustomAttributesAsComments(field)];
+ const result = model.addField(field.name, type, attributes, docs, addToFront);
if (this.mode === 'logical') {
if (field.attributes.some((attr) => isDefaultWithAuth(attr))) {
@@ -777,8 +843,6 @@ export class PrismaSchemaGenerator {
}
}
- // user defined comments pass-through
- field.comments.forEach((c) => result.addComment(c));
return result;
}
@@ -898,12 +962,9 @@ export class PrismaSchemaGenerator {
this.generateContainerAttribute(_enum, attr);
}
- decl.attributes
- .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr))
- .forEach((attr) => _enum.addComment('/// ' + this.zModelGenerator.generate(attr)));
-
// user defined comments pass-through
decl.comments.forEach((c) => _enum.addComment(c));
+ this.getCustomAttributesAsComments(decl).forEach((c) => _enum.addComment(c));
}
private generateEnumField(_enum: PrismaEnum, field: EnumField) {
@@ -911,10 +972,18 @@ export class PrismaSchemaGenerator {
.filter((attr) => this.isPrismaAttribute(attr))
.map((attr) => this.makeFieldAttribute(attr));
- const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr));
+ const docs = [...field.comments, ...this.getCustomAttributesAsComments(field)];
+ _enum.addField(field.name, attributes, docs);
+ }
- const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generate(attr));
- _enum.addField(field.name, attributes, documentations.concat(field.comments));
+ private getCustomAttributesAsComments(decl: DataModel | DataModelField | Enum | EnumField) {
+ if (!this.customAttributesAsComments) {
+ return [];
+ } else {
+ return decl.attributes
+ .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr))
+ .map((attr) => `/// ${this.zModelGenerator.generate(attr)}`);
+ }
}
}
diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts
index ca26ffabe..01a5920ff 100644
--- a/packages/schema/src/plugins/zod/generator.ts
+++ b/packages/schema/src/plugins/zod/generator.ts
@@ -14,12 +14,12 @@ import {
parseOptionAsStrings,
resolvePath,
} from '@zenstackhq/sdk';
-import { DataModel, EnumField, Model, isDataModel, isEnum } from '@zenstackhq/sdk/ast';
+import { DataModel, EnumField, Model, TypeDef, isDataModel, isEnum, isTypeDef } from '@zenstackhq/sdk/ast';
import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers';
import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma';
import { streamAllContents } from 'langium';
import path from 'path';
-import type { SourceFile } from 'ts-morph';
+import type { CodeBlockWriter, SourceFile } from 'ts-morph';
import { upperCaseFirst } from 'upper-case-first';
import { name } from '.';
import { getDefaultOutputFolder } from '../plugin-utils';
@@ -274,6 +274,12 @@ export class ZodSchemaGenerator {
}
}
+ for (const typeDef of this.model.declarations.filter(isTypeDef)) {
+ if (!excludedModels.includes(typeDef.name)) {
+ schemaNames.push(await this.generateTypeDefSchema(typeDef, output));
+ }
+ }
+
this.sourceFiles.push(
this.project.createSourceFile(
path.join(output, 'models', 'index.ts'),
@@ -283,6 +289,89 @@ export class ZodSchemaGenerator {
);
}
+ private generateTypeDefSchema(typeDef: TypeDef, output: string) {
+ const schemaName = `${upperCaseFirst(typeDef.name)}.schema`;
+ const sf = this.project.createSourceFile(path.join(output, 'models', `${schemaName}.ts`), undefined, {
+ overwrite: true,
+ });
+ this.sourceFiles.push(sf);
+ sf.replaceWithText((writer) => {
+ this.addPreludeAndImports(typeDef, writer, output);
+
+ writer.write(`export const ${typeDef.name}Schema = z.object(`);
+ writer.inlineBlock(() => {
+ typeDef.fields.forEach((field) => {
+ writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`);
+ });
+ });
+
+ switch (this.options.mode) {
+ case 'strip':
+ // zod strips by default
+ writer.writeLine(')');
+ break;
+ case 'passthrough':
+ writer.writeLine(').passthrough();');
+ break;
+ default:
+ writer.writeLine(').strict();');
+ break;
+ }
+ });
+
+ // TODO: "@@validate" refinements
+
+ return schemaName;
+ }
+
+ private addPreludeAndImports(decl: DataModel | TypeDef, writer: CodeBlockWriter, output: string) {
+ writer.writeLine('/* eslint-disable */');
+ writer.writeLine(`import { z } from 'zod';`);
+
+ // import user-defined enums from Prisma as they might be referenced in the expressions
+ const importEnums = new Set();
+ for (const node of streamAllContents(decl)) {
+ if (isEnumFieldReference(node)) {
+ const field = node.target.ref as EnumField;
+ if (!isFromStdlib(field.$container)) {
+ importEnums.add(field.$container.name);
+ }
+ }
+ }
+ if (importEnums.size > 0) {
+ const prismaImport = computePrismaClientImport(path.join(output, 'models'), this.options);
+ writer.writeLine(`import { ${[...importEnums].join(', ')} } from '${prismaImport}';`);
+ }
+
+ // import enum schemas
+ const importedEnumSchemas = new Set();
+ for (const field of decl.fields) {
+ if (field.type.reference?.ref && isEnum(field.type.reference?.ref)) {
+ const name = upperCaseFirst(field.type.reference?.ref.name);
+ if (!importedEnumSchemas.has(name)) {
+ writer.writeLine(`import { ${name}Schema } from '../enums/${name}.schema';`);
+ importedEnumSchemas.add(name);
+ }
+ }
+ }
+
+ // import Decimal
+ if (decl.fields.some((field) => field.type.type === 'Decimal')) {
+ writer.writeLine(`import { DecimalSchema } from '../common';`);
+ writer.writeLine(`import { Decimal } from 'decimal.js';`);
+ }
+
+ // import referenced types' schemas
+ const referencedTypes = new Set(
+ decl.fields
+ .filter((field) => isTypeDef(field.type.reference?.ref) && field.type.reference?.ref.name !== decl.name)
+ .map((field) => field.type.reference!.ref!.name)
+ );
+ for (const refType of referencedTypes) {
+ writer.writeLine(`import { ${upperCaseFirst(refType)}Schema } from './${upperCaseFirst(refType)}.schema';`);
+ }
+ }
+
private async generateModelSchema(model: DataModel, output: string) {
const schemaName = `${upperCaseFirst(model.name)}.schema`;
const sf = this.project.createSourceFile(path.join(output, 'models', `${schemaName}.ts`), undefined, {
@@ -301,41 +390,7 @@ export class ZodSchemaGenerator {
const relations = model.fields.filter((field) => isDataModel(field.type.reference?.ref));
const fkFields = model.fields.filter((field) => isForeignKeyField(field));
- writer.writeLine('/* eslint-disable */');
- writer.writeLine(`import { z } from 'zod';`);
-
- // import user-defined enums from Prisma as they might be referenced in the expressions
- const importEnums = new Set();
- for (const node of streamAllContents(model)) {
- if (isEnumFieldReference(node)) {
- const field = node.target.ref as EnumField;
- if (!isFromStdlib(field.$container)) {
- importEnums.add(field.$container.name);
- }
- }
- }
- if (importEnums.size > 0) {
- const prismaImport = computePrismaClientImport(path.join(output, 'models'), this.options);
- writer.writeLine(`import { ${[...importEnums].join(', ')} } from '${prismaImport}';`);
- }
-
- // import enum schemas
- const importedEnumSchemas = new Set();
- for (const field of scalarFields) {
- if (field.type.reference?.ref && isEnum(field.type.reference?.ref)) {
- const name = upperCaseFirst(field.type.reference?.ref.name);
- if (!importedEnumSchemas.has(name)) {
- writer.writeLine(`import { ${name}Schema } from '../enums/${name}.schema';`);
- importedEnumSchemas.add(name);
- }
- }
- }
-
- // import Decimal
- if (scalarFields.some((field) => field.type.type === 'Decimal')) {
- writer.writeLine(`import { DecimalSchema } from '../common';`);
- writer.writeLine(`import { Decimal } from 'decimal.js';`);
- }
+ this.addPreludeAndImports(model, writer, output);
// base schema - including all scalar fields, with optionality following the schema
writer.write(`const baseSchema = z.object(`);
diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts
index 698ad2ac6..6b83e5723 100644
--- a/packages/schema/src/plugins/zod/transformer.ts
+++ b/packages/schema/src/plugins/zod/transformer.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { indentString, isDiscriminatorField, type PluginOptions } from '@zenstackhq/sdk';
-import { DataModel, isDataModel, type Model } from '@zenstackhq/sdk/ast';
+import { DataModel, isDataModel, isTypeDef, type Model } from '@zenstackhq/sdk/ast';
import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers';
import { supportCreateMany, type DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma';
import path from 'path';
@@ -88,29 +88,33 @@ export default class Transformer {
}
generateObjectSchema(generateUnchecked: boolean, options: PluginOptions) {
- const zodObjectSchemaFields = this.generateObjectSchemaFields(generateUnchecked);
- const objectSchema = this.prepareObjectSchema(zodObjectSchemaFields, options);
+ const { schemaFields, extraImports } = this.generateObjectSchemaFields(generateUnchecked);
+ const objectSchema = this.prepareObjectSchema(schemaFields, options);
const filePath = path.join(Transformer.outputPath, `objects/${this.name}.schema.ts`);
- const content = '/* eslint-disable */\n' + objectSchema;
+ const content = '/* eslint-disable */\n' + extraImports.join('\n\n') + objectSchema;
this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true }));
return `${this.name}.schema`;
}
- private delegateCreateUpdateInputRegex = /(\S+)(Unchecked)?(Create|Update).*Input/;
+ private createUpdateInputRegex = /(\S+?)(Unchecked)?(Create|Update|CreateMany|UpdateMany).*Input/;
generateObjectSchemaFields(generateUnchecked: boolean) {
let fields = this.fields;
+ let contextDataModel: DataModel | undefined;
+ const extraImports: string[] = [];
// exclude discriminator fields from create/update input schemas
- const createUpdateMatch = this.delegateCreateUpdateInputRegex.exec(this.name);
+ const createUpdateMatch = this.createUpdateInputRegex.exec(this.name);
if (createUpdateMatch) {
const modelName = createUpdateMatch[1];
- const dataModel = this.zmodel.declarations.find(
+ contextDataModel = this.zmodel.declarations.find(
(d): d is DataModel => isDataModel(d) && d.name === modelName
);
- if (dataModel) {
- const discriminatorFields = dataModel.fields.filter(isDiscriminatorField);
+
+ if (contextDataModel) {
+ // exclude discriminator fields from create/update input schemas
+ const discriminatorFields = contextDataModel.fields.filter(isDiscriminatorField);
if (discriminatorFields.length > 0) {
fields = fields.filter((field) => {
return !discriminatorFields.some(
@@ -118,11 +122,23 @@ export default class Transformer {
);
});
}
+
+ // import type-def's schemas
+ const typeDefFields = contextDataModel.fields.filter((f) => isTypeDef(f.type.reference?.ref));
+ typeDefFields.forEach((field) => {
+ const typeName = upperCaseFirst(field.type.reference!.$refText);
+ const importLine = `import { ${typeName}Schema } from '../models/${typeName}.schema';`;
+ if (!extraImports.includes(importLine)) {
+ extraImports.push(importLine);
+ }
+ });
}
}
const zodObjectSchemaFields = fields
- .map((field) => this.generateObjectSchemaField(field, generateUnchecked))
+ .map((field) =>
+ this.generateObjectSchemaField(field, contextDataModel, generateUnchecked, !!createUpdateMatch)
+ )
.flatMap((item) => item)
.map((item) => {
const [zodStringWithMainType, field, skipValidators] = item;
@@ -133,12 +149,14 @@ export default class Transformer {
return value.trim();
});
- return zodObjectSchemaFields;
+ return { schemaFields: zodObjectSchemaFields, extraImports };
}
generateObjectSchemaField(
field: PrismaDMMF.SchemaArg,
- generateUnchecked: boolean
+ contextDataModel: DataModel | undefined,
+ generateUnchecked: boolean,
+ replaceJsonWithTypeDef = false
): [string, PrismaDMMF.SchemaArg, boolean][] {
const lines = field.inputTypes;
@@ -146,64 +164,75 @@ export default class Transformer {
return [];
}
- let alternatives = lines.reduce((result, inputType) => {
- if (!generateUnchecked && typeof inputType.type === 'string' && inputType.type.includes('Unchecked')) {
- return result;
- }
+ let alternatives: string[] | undefined = undefined;
- if (inputType.type.includes('CreateMany') && !supportCreateMany(this.zmodel)) {
- return result;
+ if (replaceJsonWithTypeDef) {
+ const dmField = contextDataModel?.fields.find((f) => f.name === field.name);
+ if (isTypeDef(dmField?.type.reference?.ref)) {
+ alternatives = [`z.lazy(() => ${upperCaseFirst(dmField?.type.reference!.$refText)}Schema)`];
}
+ }
- // TODO: unify the following with `schema-gen.ts`
-
- if (inputType.type === 'String') {
- result.push(this.wrapWithZodValidators('z.string()', field, inputType));
- } else if (inputType.type === 'Int' || inputType.type === 'Float') {
- result.push(this.wrapWithZodValidators('z.number()', field, inputType));
- } else if (inputType.type === 'Decimal') {
- this.hasDecimal = true;
- result.push(this.wrapWithZodValidators('DecimalSchema', field, inputType));
- } else if (inputType.type === 'BigInt') {
- result.push(this.wrapWithZodValidators('z.bigint()', field, inputType));
- } else if (inputType.type === 'Boolean') {
- result.push(this.wrapWithZodValidators('z.boolean()', field, inputType));
- } else if (inputType.type === 'DateTime') {
- result.push(this.wrapWithZodValidators(['z.date()', 'z.string().datetime()'], field, inputType));
- } else if (inputType.type === 'Bytes') {
- result.push(
- this.wrapWithZodValidators(
- `z.custom(data => data instanceof Uint8Array)`,
- field,
- inputType
- )
- );
- } else if (inputType.type === 'Json') {
- this.hasJson = true;
- result.push(this.wrapWithZodValidators('jsonSchema', field, inputType));
- } else if (inputType.type === 'True') {
- result.push(this.wrapWithZodValidators('z.literal(true)', field, inputType));
- } else if (inputType.type === 'Null') {
- result.push(this.wrapWithZodValidators('z.null()', field, inputType));
- } else {
- const isEnum = inputType.location === 'enumTypes';
- const isFieldRef = inputType.location === 'fieldRefTypes';
-
- if (
- // fieldRefTypes refer to other fields in the model and don't need to be generated as part of schema
- !isFieldRef &&
- (inputType.namespace === 'prisma' || isEnum)
- ) {
- if (inputType.type !== this.originalName && typeof inputType.type === 'string') {
- this.addSchemaImport(inputType.type);
- }
+ if (!alternatives) {
+ alternatives = lines.reduce((result, inputType) => {
+ if (!generateUnchecked && typeof inputType.type === 'string' && inputType.type.includes('Unchecked')) {
+ return result;
+ }
- result.push(this.generatePrismaStringLine(field, inputType, lines.length));
+ if (inputType.type.includes('CreateMany') && !supportCreateMany(this.zmodel)) {
+ return result;
+ }
+
+ // TODO: unify the following with `schema-gen.ts`
+
+ if (inputType.type === 'String') {
+ result.push(this.wrapWithZodValidators('z.string()', field, inputType));
+ } else if (inputType.type === 'Int' || inputType.type === 'Float') {
+ result.push(this.wrapWithZodValidators('z.number()', field, inputType));
+ } else if (inputType.type === 'Decimal') {
+ this.hasDecimal = true;
+ result.push(this.wrapWithZodValidators('DecimalSchema', field, inputType));
+ } else if (inputType.type === 'BigInt') {
+ result.push(this.wrapWithZodValidators('z.bigint()', field, inputType));
+ } else if (inputType.type === 'Boolean') {
+ result.push(this.wrapWithZodValidators('z.boolean()', field, inputType));
+ } else if (inputType.type === 'DateTime') {
+ result.push(this.wrapWithZodValidators(['z.date()', 'z.string().datetime()'], field, inputType));
+ } else if (inputType.type === 'Bytes') {
+ result.push(
+ this.wrapWithZodValidators(
+ `z.custom(data => data instanceof Uint8Array)`,
+ field,
+ inputType
+ )
+ );
+ } else if (inputType.type === 'Json') {
+ this.hasJson = true;
+ result.push(this.wrapWithZodValidators('jsonSchema', field, inputType));
+ } else if (inputType.type === 'True') {
+ result.push(this.wrapWithZodValidators('z.literal(true)', field, inputType));
+ } else if (inputType.type === 'Null') {
+ result.push(this.wrapWithZodValidators('z.null()', field, inputType));
+ } else {
+ const isEnum = inputType.location === 'enumTypes';
+ const isFieldRef = inputType.location === 'fieldRefTypes';
+
+ if (
+ // fieldRefTypes refer to other fields in the model and don't need to be generated as part of schema
+ !isFieldRef &&
+ (inputType.namespace === 'prisma' || isEnum)
+ ) {
+ if (inputType.type !== this.originalName && typeof inputType.type === 'string') {
+ this.addSchemaImport(inputType.type);
+ }
+
+ result.push(this.generatePrismaStringLine(field, inputType, lines.length));
+ }
}
- }
- return result;
- }, []);
+ return result;
+ }, []);
+ }
if (alternatives.length === 0) {
return [];
diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts
index ee46390ff..c130934b2 100644
--- a/packages/schema/src/plugins/zod/utils/schema-gen.ts
+++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts
@@ -1,32 +1,34 @@
import {
ExpressionContext,
- PluginError,
- TypeScriptExpressionTransformer,
- TypeScriptExpressionTransformerError,
getAttributeArg,
getAttributeArgLiteral,
getLiteral,
getLiteralArray,
isDataModelFieldReference,
isFromStdlib,
+ PluginError,
+ TypeScriptExpressionTransformer,
+ TypeScriptExpressionTransformerError,
} from '@zenstackhq/sdk';
import {
DataModel,
DataModelField,
DataModelFieldAttribute,
- isDataModel,
isArrayExpr,
+ isBooleanLiteral,
+ isDataModel,
isEnum,
isInvocationExpr,
isNumberLiteral,
isStringLiteral,
- isBooleanLiteral
+ isTypeDef,
+ TypeDefField,
} from '@zenstackhq/sdk/ast';
import { upperCaseFirst } from 'upper-case-first';
import { name } from '..';
import { isDefaultWithAuth } from '../../enhancer/enhancer-utils';
-export function makeFieldSchema(field: DataModelField) {
+export function makeFieldSchema(field: DataModelField | TypeDefField) {
if (isDataModel(field.type.reference?.ref)) {
if (field.type.array) {
// array field is always optional
@@ -172,11 +174,17 @@ export function makeFieldSchema(field: DataModelField) {
return schema;
}
-function makeZodSchema(field: DataModelField) {
+function makeZodSchema(field: DataModelField | TypeDefField) {
let schema: string;
- if (field.type.reference?.ref && isEnum(field.type.reference?.ref)) {
- schema = `${upperCaseFirst(field.type.reference.ref.name)}Schema`;
+ if (field.type.reference?.ref) {
+ if (isEnum(field.type.reference?.ref)) {
+ schema = `${upperCaseFirst(field.type.reference.ref.name)}Schema`;
+ } else if (isTypeDef(field.type.reference?.ref)) {
+ schema = `z.lazy(() => ${upperCaseFirst(field.type.reference.ref.name)}Schema)`;
+ } else {
+ schema = 'z.any()';
+ }
} else {
switch (field.type.type) {
case 'Int':
@@ -227,7 +235,8 @@ export function makeValidationRefinements(model: DataModel) {
const message = messageArg ? `message: ${JSON.stringify(messageArg)},` : '';
const pathArg = getAttributeArg(attr, 'path');
- const path = pathArg && isArrayExpr(pathArg) ? `path: ['${getLiteralArray(pathArg)?.join(`', '`)}'],` : '';
+ const path =
+ pathArg && isArrayExpr(pathArg) ? `path: ['${getLiteralArray(pathArg)?.join(`', '`)}'],` : '';
const options = `, { ${message} ${path} }`;
@@ -272,7 +281,7 @@ function refineDecimal(op: 'gt' | 'gte' | 'lt' | 'lte', value: number, messageAr
}${messageArg})`;
}
-export function getFieldSchemaDefault(field: DataModelField) {
+export function getFieldSchemaDefault(field: DataModelField | TypeDefField) {
const attr = field.attributes.find((attr) => attr.decl.ref?.name === '@default');
if (!attr) {
return undefined;
diff --git a/packages/schema/src/res/starter.zmodel b/packages/schema/src/res/starter.zmodel
index c23cbbbeb..978724dc7 100644
--- a/packages/schema/src/res/starter.zmodel
+++ b/packages/schema/src/res/starter.zmodel
@@ -1,8 +1,6 @@
// This is a sample model to get you started.
-/**
- * A sample data source using local sqlite db.
- */
+/// A sample data source using local sqlite db.
datasource db {
provider = 'sqlite'
url = 'file:./dev.db'
@@ -12,9 +10,7 @@ generator client {
provider = "prisma-client-js"
}
-/**
- * User model
- */
+/// User model
model User {
id String @id @default(cuid())
email String @unique @email @length(6, 32)
@@ -28,9 +24,7 @@ model User {
@@allow('all', auth() == this)
}
-/**
- * Post model
- */
+/// Post model
model Post {
id String @id @default(cuid())
createdAt DateTime @default(now())
diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel
index a436ea4a8..62cefd36e 100644
--- a/packages/schema/src/res/stdlib.zmodel
+++ b/packages/schema/src/res/stdlib.zmodel
@@ -47,6 +47,7 @@ enum AttributeTargetField {
JsonField
BytesField
ModelField
+ TypeDefField
}
/**
@@ -175,6 +176,11 @@ function isEmpty(field: Any[]): Boolean {
*/
attribute @@@targetField(_ targetField: AttributeTargetField[])
+/**
+ * Marks an attribute to be applicable to type defs and fields.
+ */
+attribute @@@supportTypeDef()
+
/**
* Marks an attribute to be used for data validation.
*/
@@ -209,7 +215,7 @@ attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?)
* Defines a default value for a field.
* @param value: An expression (e.g. 5, true, now(), auth()).
*/
-attribute @default(_ value: ContextType, map: String?) @@@prisma
+attribute @default(_ value: ContextType, map: String?) @@@prisma @@@supportTypeDef
/**
* Defines a unique constraint for this field.
@@ -558,77 +564,77 @@ attribute @omit()
/**
* Validates length of a string field.
*/
-attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField]) @@@validation
+attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Validates a string field value starts with the given text.
*/
-attribute @startsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation
+attribute @startsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Validates a string field value ends with the given text.
*/
-attribute @endsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation
+attribute @endsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Validates a string field value contains the given text.
*/
-attribute @contains(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation
+attribute @contains(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Validates a string field value matches a regex.
*/
-attribute @regex(_ regex: String, _ message: String?) @@@targetField([StringField]) @@@validation
+attribute @regex(_ regex: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Validates a string field value is a valid email address.
*/
-attribute @email(_ message: String?) @@@targetField([StringField]) @@@validation
+attribute @email(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Validates a string field value is a valid ISO datetime.
*/
-attribute @datetime(_ message: String?) @@@targetField([StringField]) @@@validation
+attribute @datetime(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Validates a string field value is a valid url.
*/
-attribute @url(_ message: String?) @@@targetField([StringField]) @@@validation
+attribute @url(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Trims whitespaces from the start and end of the string.
*/
-attribute @trim() @@@targetField([StringField]) @@@validation
+attribute @trim() @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Transform entire string toLowerCase.
*/
-attribute @lower() @@@targetField([StringField]) @@@validation
+attribute @lower() @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Transform entire string toUpperCase.
*/
-attribute @upper() @@@targetField([StringField]) @@@validation
+attribute @upper() @@@targetField([StringField]) @@@validation @@@supportTypeDef
/**
* Validates a number field is greater than the given value.
*/
-attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
+attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef
/**
* Validates a number field is greater than or equal to the given value.
*/
-attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
+attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef
/**
* Validates a number field is less than the given value.
*/
-attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
+attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef
/**
* Validates a number field is less than or equal to the given value.
*/
-attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation
+attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef
/**
* Validates the entity with a complex condition.
@@ -700,3 +706,8 @@ attribute @@delegate(_ discriminator: FieldReference)
*/
function raw(value: String): Any {
} @@@expressionContext([Index])
+
+/**
+ * Marks a field to be strong-typed JSON.
+ */
+attribute @json() @@@targetField([TypeDefField])
diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts
index b040d4cf3..effd472f0 100644
--- a/packages/schema/src/utils/ast-utils.ts
+++ b/packages/schema/src/utils/ast-utils.ts
@@ -17,7 +17,13 @@ import {
ModelImport,
ReferenceExpr,
} from '@zenstackhq/language/ast';
-import { getModelFieldsWithBases, getRecursiveBases, isDelegateModel, isFromStdlib } from '@zenstackhq/sdk';
+import {
+ getInheritanceChain,
+ getModelFieldsWithBases,
+ getRecursiveBases,
+ isDelegateModel,
+ isFromStdlib,
+} from '@zenstackhq/sdk';
import {
AstNode,
copyAstNode,
@@ -61,7 +67,7 @@ export function mergeBaseModels(model: Model, linker: Linker) {
.concat(dataModel.fields);
dataModel.attributes = bases
- .flatMap((base) => base.attributes.filter((attr) => filterBaseAttribute(base, attr)))
+ .flatMap((base) => base.attributes.filter((attr) => filterBaseAttribute(dataModel, base, attr)))
.map((attr) => cloneAst(attr, dataModel, buildReference))
.concat(dataModel.attributes);
}
@@ -85,7 +91,7 @@ export function mergeBaseModels(model: Model, linker: Linker) {
linkContentToContainer(model);
}
-function filterBaseAttribute(base: DataModel, attr: DataModelAttribute) {
+function filterBaseAttribute(forModel: DataModel, base: DataModel, attr: DataModelAttribute) {
if (attr.$inheritedFrom) {
// don't inherit from skip-level base
return false;
@@ -101,13 +107,26 @@ function filterBaseAttribute(base: DataModel, attr: DataModelAttribute) {
return false;
}
- if (isDelegateModel(base) && uninheritableFromDelegateAttributes.includes(attr.decl.$refText)) {
+ if (
+ // checks if the inheritance is from a delegate model or through one, if so,
+ // the attribute shouldn't be inherited as the delegate already inherits it
+ isInheritedFromOrThroughDelegate(forModel, base) &&
+ uninheritableFromDelegateAttributes.includes(attr.decl.$refText)
+ ) {
return false;
}
return true;
}
+function isInheritedFromOrThroughDelegate(model: DataModel, base: DataModel) {
+ if (isDelegateModel(base)) {
+ return true;
+ }
+ const chain = getInheritanceChain(model, base);
+ return !!chain?.some(isDelegateModel);
+}
+
// deep clone an AST, relink references, and set its container
function cloneAst(
node: T,
diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts
index 5affcec77..b4f58dcf1 100644
--- a/packages/schema/tests/generator/prisma-generator.test.ts
+++ b/packages/schema/tests/generator/prisma-generator.test.ts
@@ -47,10 +47,35 @@ describe('Prisma generator test', () => {
provider = '@core/prisma'
}
+ /// User roles
+ enum Role {
+ /// Admin role
+ ADMIN
+ /// Regular role
+ USER
+
+ @@schema("auth")
+ }
+
+ /// My user model
+ /// defined here
model User {
- id String @id
+ /// the id field
+ id String @id @allow('read', this == auth())
+ role Role
@@schema("auth")
+ @@allow('all', true)
+ @@deny('update', this != auth())
+ }
+
+ /**
+ * My post model
+ * defined here
+ */
+ model Post {
+ id String @id
+ @@schema("public")
}
`);
@@ -60,6 +85,7 @@ describe('Prisma generator test', () => {
schemaPath: 'schema.zmodel',
output: 'schema.prisma',
format: false,
+ customAttributesAsComments: true,
});
const content = fs.readFileSync('schema.prisma', 'utf-8');
@@ -70,6 +96,14 @@ describe('Prisma generator test', () => {
'extensions = [pg_trgm, postgis(version: "3.3.2"), uuid_ossp(map: "uuid-ossp", schema: "extensions")]'
);
expect(content).toContain('schemas = ["auth", "public"]');
+ expect(content).toContain('/// My user model');
+ expect(content).toContain(`/// @@allow('all', true)`);
+ expect(content).toContain(`/// the id field`);
+ expect(content).toContain(`/// @allow('read', this == auth())`);
+ expect(content).not.toContain('/// My post model');
+ expect(content).toContain('/// User roles');
+ expect(content).toContain('/// Admin role');
+ expect(content).toContain('/// Regular role');
await getDMMF({ datamodel: content });
});
@@ -172,6 +206,7 @@ describe('Prisma generator test', () => {
provider: '@core/prisma',
schemaPath: 'schema.zmodel',
output: name,
+ customAttributesAsComments: true,
});
const content = fs.readFileSync(name, 'utf-8');
@@ -204,6 +239,7 @@ describe('Prisma generator test', () => {
provider: '@core/prisma',
schemaPath: 'schema.zmodel',
output: name,
+ customAttributesAsComments: true,
});
const content = fs.readFileSync(name, 'utf-8');
@@ -397,6 +433,7 @@ describe('Prisma generator test', () => {
schemaPath: 'schema.zmodel',
output: name,
generateClient: false,
+ customAttributesAsComments: true,
});
const content = fs.readFileSync(name, 'utf-8');
@@ -447,6 +484,7 @@ describe('Prisma generator test', () => {
schemaPath: 'schema.zmodel',
output: name,
format: true,
+ customAttributesAsComments: true,
});
const content = fs.readFileSync(name, 'utf-8');
@@ -478,6 +516,7 @@ describe('Prisma generator test', () => {
schemaPath: 'schema.zmodel',
output: name,
format: true,
+ customAttributesAsComments: true,
});
const content = fs.readFileSync(name, 'utf-8');
diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts
index 0c89c61ae..4d86837d0 100644
--- a/packages/schema/tests/schema/validation/attribute-validation.test.ts
+++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts
@@ -1354,4 +1354,19 @@ describe('Attribute tests', () => {
'Field-level policy rules with "update" or "all" kind are not allowed for relation fields. Put rules on foreign-key fields instead.'
);
});
+
+ it('type def field attribute', async () => {
+ await expect(
+ loadModelWithError(`
+ model User {
+ id String @id
+ profile Profile
+ }
+
+ type Profile {
+ email String @omit
+ }
+ `)
+ ).resolves.toContain(`attribute "@omit" cannot be used on type declaration fields`);
+ });
});
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 5d9af1782..6222c5500 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/sdk",
- "version": "2.7.5",
+ "version": "2.8.0",
"description": "ZenStack plugin development SDK",
"main": "index.js",
"scripts": {
@@ -18,8 +18,8 @@
"author": "",
"license": "MIT",
"dependencies": {
- "@prisma/generator-helper": "5.21.x",
- "@prisma/internals": "5.21.x",
+ "@prisma/generator-helper": "5.22.x",
+ "@prisma/internals": "5.22.x",
"@zenstackhq/language": "workspace:*",
"@zenstackhq/runtime": "workspace:*",
"langium": "1.3.1",
diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts
index 469cfa888..859399673 100644
--- a/packages/sdk/src/model-meta-generator.ts
+++ b/packages/sdk/src/model-meta-generator.ts
@@ -6,11 +6,15 @@ import {
isArrayExpr,
isBooleanLiteral,
isDataModel,
+ isDataModelField,
isInvocationExpr,
isNumberLiteral,
isReferenceExpr,
isStringLiteral,
+ isTypeDef,
ReferenceExpr,
+ TypeDef,
+ TypeDefField,
} from '@zenstackhq/language/ast';
import type { RuntimeAttribute } from '@zenstackhq/runtime';
import { streamAst } from 'langium';
@@ -62,13 +66,18 @@ export type ModelMetaGeneratorOptions = {
shortNameMap?: Map;
};
-export async function generate(project: Project, models: DataModel[], options: ModelMetaGeneratorOptions) {
+export async function generate(
+ project: Project,
+ models: DataModel[],
+ typeDefs: TypeDef[],
+ options: ModelMetaGeneratorOptions
+) {
const sf = project.createSourceFile(options.output, undefined, { overwrite: true });
sf.addStatements('/* eslint-disable */');
sf.addVariableStatement({
declarationKind: VariableDeclarationKind.Const,
declarations: [
- { name: 'metadata', initializer: (writer) => generateModelMetadata(models, sf, writer, options) },
+ { name: 'metadata', initializer: (writer) => generateModelMetadata(models, typeDefs, sf, writer, options) },
],
});
sf.addStatements('export default metadata;');
@@ -82,12 +91,14 @@ export async function generate(project: Project, models: DataModel[], options: M
function generateModelMetadata(
dataModels: DataModel[],
+ typeDefs: TypeDef[],
sourceFile: SourceFile,
writer: CodeBlockWriter,
options: ModelMetaGeneratorOptions
) {
writer.block(() => {
writeModels(sourceFile, writer, dataModels, options);
+ writeTypeDefs(sourceFile, writer, typeDefs, options);
writeDeleteCascade(writer, dataModels);
writeShortNameMap(options, writer);
writeAuthModel(writer, dataModels);
@@ -120,6 +131,29 @@ function writeModels(
writer.writeLine(',');
}
+function writeTypeDefs(
+ sourceFile: SourceFile,
+ writer: CodeBlockWriter,
+ typedDefs: TypeDef[],
+ options: ModelMetaGeneratorOptions
+) {
+ if (typedDefs.length === 0) {
+ return;
+ }
+ writer.write('typeDefs:');
+ writer.block(() => {
+ for (const typeDef of typedDefs) {
+ writer.write(`${lowerCaseFirst(typeDef.name)}:`);
+ writer.block(() => {
+ writer.write(`name: '${typeDef.name}',`);
+ writeFields(sourceFile, writer, typeDef, options);
+ });
+ writer.writeLine(',');
+ }
+ });
+ writer.writeLine(',');
+}
+
function writeBaseTypes(writer: CodeBlockWriter, model: DataModel) {
if (model.superTypes.length > 0) {
writer.write('baseTypes: [');
@@ -189,14 +223,14 @@ function writeDiscriminator(writer: CodeBlockWriter, model: DataModel) {
function writeFields(
sourceFile: SourceFile,
writer: CodeBlockWriter,
- model: DataModel,
+ container: DataModel | TypeDef,
options: ModelMetaGeneratorOptions
) {
writer.write('fields:');
writer.block(() => {
- for (const f of model.fields) {
- const backlink = getBackLink(f);
- const fkMapping = generateForeignKeyMapping(f);
+ for (const f of container.fields) {
+ const dmField = isDataModelField(f) ? f : undefined;
+
writer.write(`${f.name}: {`);
writer.write(`
@@ -208,7 +242,7 @@ function writeFields(
f.type.type!
}",`);
- if (isIdField(f)) {
+ if (dmField && isIdField(dmField)) {
writer.write(`
isId: true,`);
}
@@ -216,6 +250,9 @@ function writeFields(
if (isDataModel(f.type.reference?.ref)) {
writer.write(`
isDataModel: true,`);
+ } else if (isTypeDef(f.type.reference?.ref)) {
+ writer.write(`
+ isTypeDef: true,`);
}
if (f.type.array) {
@@ -243,46 +280,53 @@ function writeFields(
}
}
- if (backlink) {
+ const defaultValueProvider = generateDefaultValueProvider(f, sourceFile);
+ if (defaultValueProvider) {
writer.write(`
- backLink: '${backlink.name}',`);
+ defaultValueProvider: ${defaultValueProvider},`);
}
- if (isRelationOwner(f, backlink)) {
- writer.write(`
+ if (dmField) {
+ // metadata specific to DataModelField
+
+ const backlink = getBackLink(dmField);
+ const fkMapping = generateForeignKeyMapping(dmField);
+
+ if (backlink) {
+ writer.write(`
+ backLink: '${backlink.name}',`);
+ }
+
+ if (isRelationOwner(dmField, backlink)) {
+ writer.write(`
isRelationOwner: true,`);
- }
+ }
- if (isForeignKeyField(f)) {
- writer.write(`
- isForeignKey: true,`);
- const relationField = getRelationField(f);
- if (relationField) {
+ if (isForeignKeyField(dmField)) {
writer.write(`
+ isForeignKey: true,`);
+ const relationField = getRelationField(dmField);
+ if (relationField) {
+ writer.write(`
relationField: '${relationField.name}',`);
+ }
}
- }
- if (fkMapping && Object.keys(fkMapping).length > 0) {
- writer.write(`
+ if (fkMapping && Object.keys(fkMapping).length > 0) {
+ writer.write(`
foreignKeyMapping: ${JSON.stringify(fkMapping)},`);
- }
-
- const defaultValueProvider = generateDefaultValueProvider(f, sourceFile);
- if (defaultValueProvider) {
- writer.write(`
- defaultValueProvider: ${defaultValueProvider},`);
- }
+ }
- const inheritedFromDelegate = getInheritedFromDelegate(f);
- if (inheritedFromDelegate && !isIdField(f)) {
- writer.write(`
+ const inheritedFromDelegate = getInheritedFromDelegate(dmField);
+ if (inheritedFromDelegate && !isIdField(dmField)) {
+ writer.write(`
inheritedFrom: ${JSON.stringify(inheritedFromDelegate.name)},`);
- }
+ }
- if (isAutoIncrement(f)) {
- writer.write(`
+ if (isAutoIncrement(dmField)) {
+ writer.write(`
isAutoIncrement: true,`);
+ }
}
writer.write(`
@@ -337,7 +381,7 @@ function getRelationName(field: DataModelField) {
return getAttributeArgLiteral(relAttr, 'name');
}
-function getAttributes(target: DataModelField | DataModel): RuntimeAttribute[] {
+function getAttributes(target: DataModelField | DataModel | TypeDefField): RuntimeAttribute[] {
return target.attributes
.map((attr) => {
const args: Array<{ name?: string; value: unknown }> = [];
@@ -498,7 +542,7 @@ function getDeleteCascades(model: DataModel): string[] {
.map((m) => m.name);
}
-function generateDefaultValueProvider(field: DataModelField, sourceFile: SourceFile) {
+function generateDefaultValueProvider(field: DataModelField | TypeDefField, sourceFile: SourceFile) {
const defaultAttr = getAttribute(field, '@default');
if (!defaultAttr) {
return undefined;
diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts
index d6c4c0fd5..6b2bfe868 100644
--- a/packages/sdk/src/utils.ts
+++ b/packages/sdk/src/utils.ts
@@ -30,6 +30,7 @@ import {
Model,
Reference,
ReferenceExpr,
+ TypeDefField,
} from '@zenstackhq/language/ast';
import fs from 'node:fs';
import path from 'path';
@@ -123,7 +124,7 @@ export function hasAttribute(
}
export function getAttribute(
- decl: DataModel | DataModelField | Enum | EnumField | FunctionDecl | Attribute | AttributeParam,
+ decl: DataModel | DataModelField | TypeDefField | Enum | EnumField | FunctionDecl | Attribute | AttributeParam,
name: string
) {
return (decl.attributes as (DataModelAttribute | DataModelFieldAttribute)[]).find(
@@ -460,6 +461,9 @@ export function isDelegateModel(node: AstNode) {
}
export function isDiscriminatorField(field: DataModelField) {
+ if (!isDataModel(field.$container)) {
+ return false;
+ }
const model = field.$inheritedFrom ?? field.$container;
const delegateAttr = getAttribute(model, '@@delegate');
if (!delegateAttr) {
@@ -569,3 +573,24 @@ export function getInheritedFromDelegate(field: DataModelField) {
const foundBase = bases.findLast((base) => base.fields.some((f) => f.name === field.name) && isDelegateModel(base));
return foundBase;
}
+
+/**
+ * Gets the inheritance chain from "from" to "to", excluding them.
+ */
+export function getInheritanceChain(from: DataModel, to: DataModel): DataModel[] | undefined {
+ if (from === to) {
+ return [];
+ }
+
+ for (const base of from.superTypes) {
+ if (base.ref === to) {
+ return [];
+ }
+ const path = getInheritanceChain(base.ref!, to);
+ if (path) {
+ return [base.ref as DataModel, ...path];
+ }
+ }
+
+ return undefined;
+}
diff --git a/packages/sdk/src/validation.ts b/packages/sdk/src/validation.ts
index e7edc21fc..9dbcd8f5c 100644
--- a/packages/sdk/src/validation.ts
+++ b/packages/sdk/src/validation.ts
@@ -1,4 +1,11 @@
-import type { DataModel, DataModelAttribute, DataModelFieldAttribute } from './ast';
+import {
+ isDataModel,
+ isTypeDef,
+ type DataModel,
+ type DataModelAttribute,
+ type DataModelFieldAttribute,
+ type TypeDef,
+} from './ast';
function isValidationAttribute(attr: DataModelAttribute | DataModelFieldAttribute) {
return attr.decl.ref?.attributes.some((attr) => attr.decl.$refText === '@@@validation');
@@ -8,12 +15,30 @@ function isValidationAttribute(attr: DataModelAttribute | DataModelFieldAttribut
* Returns if the given model contains any data validation rules (both at the model
* level and at the field level).
*/
-export function hasValidationAttributes(model: DataModel) {
- if (model.attributes.some((attr) => isValidationAttribute(attr))) {
- return true;
+export function hasValidationAttributes(
+ decl: DataModel | TypeDef,
+ seen: Set = new Set()
+): boolean {
+ if (seen.has(decl)) {
+ return false;
+ }
+ seen.add(decl);
+
+ if (isDataModel(decl)) {
+ if (decl.attributes.some((attr) => isValidationAttribute(attr))) {
+ return true;
+ }
}
- if (model.fields.some((field) => field.attributes.some((attr) => isValidationAttribute(attr)))) {
+ if (
+ decl.fields.some((field) => {
+ if (isTypeDef(field.type.reference?.ref)) {
+ return hasValidationAttributes(field.type.reference?.ref, seen);
+ } else {
+ return field.attributes.some((attr) => isValidationAttribute(attr));
+ }
+ })
+ ) {
return true;
}
diff --git a/packages/server/package.json b/packages/server/package.json
index 05f905117..064fd1ea1 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/server",
- "version": "2.7.5",
+ "version": "2.8.0",
"displayName": "ZenStack Server-side Adapters",
"description": "ZenStack server-side adapters",
"homepage": "https://zenstack.dev",
diff --git a/packages/testtools/package.json b/packages/testtools/package.json
index 47cf64447..ec5208c81 100644
--- a/packages/testtools/package.json
+++ b/packages/testtools/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/testtools",
- "version": "2.7.5",
+ "version": "2.8.0",
"description": "ZenStack Test Tools",
"main": "index.js",
"private": true,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6771d2899..292cc7495 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -392,8 +392,8 @@ importers:
packages/runtime:
dependencies:
'@prisma/client':
- specifier: 5.0.0 - 5.21.x
- version: 5.21.0(prisma@5.16.1)
+ specifier: 5.0.0 - 5.22.x
+ version: 5.22.0(prisma@5.16.1)
bcryptjs:
specifier: ^2.4.3
version: 2.4.3
@@ -523,7 +523,7 @@ importers:
specifier: ^4.0.0
version: 4.0.1
prisma:
- specifier: 5.0.0 - 5.21.x
+ specifier: 5.0.0 - 5.22.x
version: 5.16.1
semver:
specifier: ^7.5.2
@@ -575,8 +575,8 @@ importers:
version: 1.5.0(zod@3.23.8)
devDependencies:
'@prisma/client':
- specifier: 5.21.x
- version: 5.21.0(prisma@5.16.1)
+ specifier: 5.22.x
+ version: 5.22.0(prisma@5.16.1)
'@types/async-exit-hook':
specifier: ^2.0.0
version: 2.0.2
@@ -627,11 +627,11 @@ importers:
packages/sdk:
dependencies:
'@prisma/generator-helper':
- specifier: 5.21.x
- version: 5.21.0
+ specifier: 5.22.x
+ version: 5.22.0
'@prisma/internals':
- specifier: 5.21.x
- version: 5.21.0
+ specifier: 5.22.x
+ version: 5.22.0
'@zenstackhq/language':
specifier: workspace:*
version: link:../language/dist
@@ -706,7 +706,7 @@ importers:
version: 10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9)
'@nestjs/testing':
specifier: ^10.3.7
- version: 10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9)(@nestjs/platform-express@10.3.9)
+ version: 10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.9)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9))
'@sveltejs/kit':
specifier: 1.21.0
version: 1.21.0(svelte@4.2.18)(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1))
@@ -2442,8 +2442,8 @@ packages:
prisma:
optional: true
- '@prisma/client@5.21.0':
- resolution: {integrity: sha512-Qf2YleB3dsCRXiKSQZ+j0aF5E+ojpfqF8ExXaNAWBbAhKapMduwNvgo13K2pEuTiQq8voG2aQPQnxjsO9fOpTQ==}
+ '@prisma/client@5.22.0':
+ resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
engines: {node: '>=16.13'}
peerDependencies:
prisma: '*'
@@ -2457,8 +2457,8 @@ packages:
'@prisma/debug@5.16.1':
resolution: {integrity: sha512-JsNgZAg6BD9RInLSrg7ZYzo11N7cVvYArq3fHGSD89HSgtN0VDdjV6bib7YddbcO6snzjchTiLfjeTqBjtArVQ==}
- '@prisma/debug@5.21.0':
- resolution: {integrity: sha512-8gX68E36OKImh7LBz5fFIuTRLZgM1ObnDA8ukhC1kZvTK7k7Unti6pJe3ZiudzuFAxae06PV1rhq1u9DZbXVnQ==}
+ '@prisma/debug@5.22.0':
+ resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==}
'@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48':
resolution: {integrity: sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==}
@@ -2466,8 +2466,8 @@ packages:
'@prisma/engines-version@5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303':
resolution: {integrity: sha512-HkT2WbfmFZ9WUPyuJHhkiADxazHg8Y4gByrTSVeb3OikP6tjQ7txtSUGu9OBOBH0C13dPKN2qqH12xKtHu/Hiw==}
- '@prisma/engines-version@5.21.0-36.08713a93b99d58f31485621c634b04983ae01d95':
- resolution: {integrity: sha512-hfq7c8MnkhcZTY0bGXG6bV5Cr7OsnHLERNy4xkZy6rbpWnhtfjuj3yUVM4u1GKXd6uWmFbg0+HDw8KXTgTVepQ==}
+ '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2':
+ resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==}
'@prisma/engines@5.14.0':
resolution: {integrity: sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==}
@@ -2475,8 +2475,8 @@ packages:
'@prisma/engines@5.16.1':
resolution: {integrity: sha512-KkyF3eIUtBIyp5A/rJHCtwQO18OjpGgx18PzjyGcJDY/+vNgaVyuVd+TgwBgeq6NLdd1XMwRCI+58vinHsAdfA==}
- '@prisma/engines@5.21.0':
- resolution: {integrity: sha512-IBewQJiDnFiz39pl8kEIzmzV4RAoBPBD2DoLDntMMXObg1an90Dp+xeb1mmwrTgRDE3elu/LYxyVPEkKw9LZ7A==}
+ '@prisma/engines@5.22.0':
+ resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==}
'@prisma/fetch-engine@5.14.0':
resolution: {integrity: sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==}
@@ -2484,14 +2484,14 @@ packages:
'@prisma/fetch-engine@5.16.1':
resolution: {integrity: sha512-oOkjaPU1lhcA/Rvr4GVfd1NLJBwExgNBE36Ueq7dr71kTMwy++a3U3oLd2ZwrV9dj9xoP6LjCcky799D9nEt4w==}
- '@prisma/fetch-engine@5.21.0':
- resolution: {integrity: sha512-nXKJrsxVKng6yjJzl7vBjrr3S34cOmWQ9SiGTo9xidVTmVSgg5GCTwDL4r2be8DE3RntqK5BW2LWQ1gF80eINw==}
+ '@prisma/fetch-engine@5.22.0':
+ resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==}
'@prisma/generator-helper@5.14.0':
resolution: {integrity: sha512-xVc71cmTnPZ0lnSs4FAY6Ta72vFJ3webrQwKMQ2ujr6hDG1VPIEf820T1TOS3ZZQd/OKigNKXnq3co8biz9/qw==}
- '@prisma/generator-helper@5.21.0':
- resolution: {integrity: sha512-+DnQBW6LwsDIpj6hDAPbWoQBwU5MP+qrDt/d5wFAhsMNqg56XgSj6ZbHEkaej58xIuae5Pg6XmzwRdBPg7f/jA==}
+ '@prisma/generator-helper@5.22.0':
+ resolution: {integrity: sha512-LwqcBQ5/QsuAaLNQZAIVIAJDJBMjHwMwn16e06IYx/3Okj/xEEfw9IvrqB2cJCl3b2mCBlh3eVH0w9WGmi4aHg==}
'@prisma/get-platform@5.14.0':
resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==}
@@ -2499,14 +2499,14 @@ packages:
'@prisma/get-platform@5.16.1':
resolution: {integrity: sha512-R4IKnWnMkR2nUAbU5gjrPehdQYUUd7RENFD2/D+xXTNhcqczp0N+WEGQ3ViyI3+6mtVcjjNIMdnUTNyu3GxIgA==}
- '@prisma/get-platform@5.21.0':
- resolution: {integrity: sha512-NAyaAcHJhs0IysGYJtM6Fm3ccEs/LkCZqz/8riVkkJswFrRtFV93jAUIVKWO/wj1Ca1gO7HaMd/tr6e/9Xmvww==}
+ '@prisma/get-platform@5.22.0':
+ resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==}
'@prisma/internals@5.14.0':
resolution: {integrity: sha512-s0JRNDmR2bvcyy0toz89jy7SbbjANAs4e9KCReNvSm5czctIaZzDf68tcOXdtH0G7m9mKhVhNPdS9lMky0DhWA==}
- '@prisma/internals@5.21.0':
- resolution: {integrity: sha512-sfMmfp9qke/imXNAL0z2gvHUZ7jPX19w7Nh4TVcvKqbTpgTk1iM1uIPQM8CIPOBEV09UwipHe3ln9yxWYqJ6gw==}
+ '@prisma/internals@5.22.0':
+ resolution: {integrity: sha512-Rsjw2ARB9VQzDczzEimUriSBdXmYG/Z5tNRer2IEwof/O8Q6A9cqV3oNVUpJ52TgWfQqMAq5K/KEf8LvvYLLOw==}
'@prisma/prisma-schema-wasm@5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85':
resolution: {integrity: sha512-SX9vE9dGYBap6xsfJuDE5b2eoA6w1vKsx8QpLUHZR+kIV6GQVUYUboEfkvYYoBVen3s9LqxJ1+LjHL/1MqBZag==}
@@ -2514,14 +2514,14 @@ packages:
'@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48':
resolution: {integrity: sha512-WeTmJ0mK8ALoKJUQFO+465k9lm1JWS4ODUg7akJq1wjgyDU1RTAzDFli8ESmNJlMVgJgoAd6jXmzcnoA0HT9Lg==}
- '@prisma/prisma-schema-wasm@5.21.0-36.08713a93b99d58f31485621c634b04983ae01d95':
- resolution: {integrity: sha512-JrZMlaWugM4JW6uiB4WirFmfMMnHnCN3LGWVTb32x2R23jPB2IBYDT61BnW8PlackUZE635IDBT8mrZ7bRy1KQ==}
+ '@prisma/prisma-schema-wasm@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2':
+ resolution: {integrity: sha512-WPNB7SgTxF/rSHMa5o5/9AIINy4oVnRhvUkRzqR4Nfp8Hu9Q2IyUptxuiDuzRVJdjJBRi/U82sHTxyiD3oBBhQ==}
'@prisma/schema-files-loader@5.14.0':
resolution: {integrity: sha512-n1QHR2C63dARKPZe0WPn7biybcBHzXe+BEmiHC5Drq9KPWnpmQtIfGpqm1ZKdvCZfcA5FF3wgpSMPK4LnB0obQ==}
- '@prisma/schema-files-loader@5.21.0':
- resolution: {integrity: sha512-snwMUFvyC+ukJWGU6xp9aGK2mXQDu8Zn6N3CB/2O73+qYfTaTvN91z0/Pk7xrUV8tdKihRk6XCyzNEODs4YfhQ==}
+ '@prisma/schema-files-loader@5.22.0':
+ resolution: {integrity: sha512-/TNAJXvMSk6mCgZa+gIBM6sp5OUQBnb7rbjiSQm88gvcSibxEuKkVV/2pT3RmQpEAn1yiabvS4+dOvIotYe3ww==}
'@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
@@ -3986,7 +3986,7 @@ packages:
engines: {node: '>= 14'}
concat-map@0.0.1:
- resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
@@ -4424,7 +4424,7 @@ packages:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
ee-first@1.1.1:
- resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+ resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
electron-to-chromium@1.4.814:
resolution: {integrity: sha512-GVulpHjFu1Y9ZvikvbArHmAhZXtm3wHlpjTMcXNGKl4IQ4jMQjlnz8yMQYYqdLHKi/jEL2+CBC2akWVCoIGUdw==}
@@ -6100,7 +6100,7 @@ packages:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
media-typer@0.3.0:
- resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
+ resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=}
engines: {node: '>= 0.6'}
merge-descriptors@1.0.1:
@@ -8260,7 +8260,7 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utils-merge@1.0.1:
- resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
+ resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=}
engines: {node: '>= 0.4.0'}
uuid@10.0.0:
@@ -10080,7 +10080,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@nestjs/testing@10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9)(@nestjs/platform-express@10.3.9)':
+ '@nestjs/testing@10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.9)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.9))':
dependencies:
'@nestjs/common': 10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core': 10.3.9(@nestjs/common@10.3.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.9)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
@@ -10608,7 +10608,7 @@ snapshots:
optionalDependencies:
prisma: 5.16.1
- '@prisma/client@5.21.0(prisma@5.16.1)':
+ '@prisma/client@5.22.0(prisma@5.16.1)':
optionalDependencies:
prisma: 5.16.1
@@ -10616,13 +10616,13 @@ snapshots:
'@prisma/debug@5.16.1': {}
- '@prisma/debug@5.21.0': {}
+ '@prisma/debug@5.22.0': {}
'@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {}
'@prisma/engines-version@5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303': {}
- '@prisma/engines-version@5.21.0-36.08713a93b99d58f31485621c634b04983ae01d95': {}
+ '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {}
'@prisma/engines@5.14.0':
dependencies:
@@ -10638,12 +10638,12 @@ snapshots:
'@prisma/fetch-engine': 5.16.1
'@prisma/get-platform': 5.16.1
- '@prisma/engines@5.21.0':
+ '@prisma/engines@5.22.0':
dependencies:
- '@prisma/debug': 5.21.0
- '@prisma/engines-version': 5.21.0-36.08713a93b99d58f31485621c634b04983ae01d95
- '@prisma/fetch-engine': 5.21.0
- '@prisma/get-platform': 5.21.0
+ '@prisma/debug': 5.22.0
+ '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2
+ '@prisma/fetch-engine': 5.22.0
+ '@prisma/get-platform': 5.22.0
'@prisma/fetch-engine@5.14.0':
dependencies:
@@ -10657,19 +10657,19 @@ snapshots:
'@prisma/engines-version': 5.16.0-24.34ace0eb2704183d2c05b60b52fba5c43c13f303
'@prisma/get-platform': 5.16.1
- '@prisma/fetch-engine@5.21.0':
+ '@prisma/fetch-engine@5.22.0':
dependencies:
- '@prisma/debug': 5.21.0
- '@prisma/engines-version': 5.21.0-36.08713a93b99d58f31485621c634b04983ae01d95
- '@prisma/get-platform': 5.21.0
+ '@prisma/debug': 5.22.0
+ '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2
+ '@prisma/get-platform': 5.22.0
'@prisma/generator-helper@5.14.0':
dependencies:
'@prisma/debug': 5.14.0
- '@prisma/generator-helper@5.21.0':
+ '@prisma/generator-helper@5.22.0':
dependencies:
- '@prisma/debug': 5.21.0
+ '@prisma/debug': 5.22.0
'@prisma/get-platform@5.14.0':
dependencies:
@@ -10679,9 +10679,9 @@ snapshots:
dependencies:
'@prisma/debug': 5.16.1
- '@prisma/get-platform@5.21.0':
+ '@prisma/get-platform@5.22.0':
dependencies:
- '@prisma/debug': 5.21.0
+ '@prisma/debug': 5.22.0
'@prisma/internals@5.14.0':
dependencies:
@@ -10695,15 +10695,15 @@ snapshots:
arg: 5.0.2
prompts: 2.4.2
- '@prisma/internals@5.21.0':
+ '@prisma/internals@5.22.0':
dependencies:
- '@prisma/debug': 5.21.0
- '@prisma/engines': 5.21.0
- '@prisma/fetch-engine': 5.21.0
- '@prisma/generator-helper': 5.21.0
- '@prisma/get-platform': 5.21.0
- '@prisma/prisma-schema-wasm': 5.21.0-36.08713a93b99d58f31485621c634b04983ae01d95
- '@prisma/schema-files-loader': 5.21.0
+ '@prisma/debug': 5.22.0
+ '@prisma/engines': 5.22.0
+ '@prisma/fetch-engine': 5.22.0
+ '@prisma/generator-helper': 5.22.0
+ '@prisma/get-platform': 5.22.0
+ '@prisma/prisma-schema-wasm': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2
+ '@prisma/schema-files-loader': 5.22.0
arg: 5.0.2
prompts: 2.4.2
@@ -10711,16 +10711,16 @@ snapshots:
'@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {}
- '@prisma/prisma-schema-wasm@5.21.0-36.08713a93b99d58f31485621c634b04983ae01d95': {}
+ '@prisma/prisma-schema-wasm@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {}
'@prisma/schema-files-loader@5.14.0':
dependencies:
'@prisma/prisma-schema-wasm': 5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85
fs-extra: 11.1.1
- '@prisma/schema-files-loader@5.21.0':
+ '@prisma/schema-files-loader@5.22.0':
dependencies:
- '@prisma/prisma-schema-wasm': 5.21.0-36.08713a93b99d58f31485621c634b04983ae01d95
+ '@prisma/prisma-schema-wasm': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2
fs-extra: 11.1.1
'@protobufjs/aspromise@1.1.2': {}
diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts
index a4cf21968..ec1525be0 100644
--- a/script/test-scaffold.ts
+++ b/script/test-scaffold.ts
@@ -19,6 +19,6 @@ function run(cmd: string) {
}
run('npm init -y');
-run('npm i --no-audit --no-fund typescript prisma@5.21.x @prisma/client@5.21.x zod decimal.js @types/node');
+run('npm i --no-audit --no-fund typescript prisma@5.22.x @prisma/client@5.22.x zod decimal.js @types/node');
console.log('Test scaffold setup complete.');
diff --git a/tests/integration/test-run/package.json b/tests/integration/test-run/package.json
index fa113f1e8..60914bc6f 100644
--- a/tests/integration/test-run/package.json
+++ b/tests/integration/test-run/package.json
@@ -10,9 +10,9 @@
"author": "",
"license": "ISC",
"dependencies": {
- "@prisma/client": "5.21.x",
+ "@prisma/client": "5.22.x",
"@zenstackhq/runtime": "file:../../../packages/runtime/dist",
- "prisma": "5.21.x",
+ "prisma": "5.22.x",
"react": "^18.2.0",
"swr": "^1.3.0",
"typescript": "^4.9.3",
diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts
index 8c04eb67e..58237dfc1 100644
--- a/tests/integration/tests/cli/plugins.test.ts
+++ b/tests/integration/tests/cli/plugins.test.ts
@@ -75,7 +75,7 @@ describe('CLI Plugins Tests', () => {
'swr',
'@tanstack/react-query@5.56.x',
'@trpc/server',
- '@prisma/client@5.21.x',
+ '@prisma/client@5.22.x',
`${path.join(__dirname, '../../../../.build/zenstackhq-language-' + ver + '.tgz')}`,
`${path.join(__dirname, '../../../../.build/zenstackhq-sdk-' + ver + '.tgz')}`,
`${path.join(__dirname, '../../../../.build/zenstackhq-runtime-' + ver + '.tgz')}`,
@@ -85,7 +85,7 @@ describe('CLI Plugins Tests', () => {
const devDepPkgs = [
'typescript',
'@types/react',
- 'prisma@5.21.x',
+ 'prisma@5.22.x',
`${path.join(__dirname, '../../../../.build/zenstack-' + ver + '.tgz')}`,
`${path.join(__dirname, '../../../../.build/zenstackhq-tanstack-query-' + ver + '.tgz')}`,
`${path.join(__dirname, '../../../../.build/zenstackhq-swr-' + ver + '.tgz')}`,
diff --git a/tests/integration/tests/enhancements/json/crud.test.ts b/tests/integration/tests/enhancements/json/crud.test.ts
new file mode 100644
index 000000000..af3705a95
--- /dev/null
+++ b/tests/integration/tests/enhancements/json/crud.test.ts
@@ -0,0 +1,270 @@
+import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools';
+
+describe('Json field CRUD', () => {
+ let dbUrl: string;
+ let prisma: any;
+
+ beforeEach(async () => {
+ dbUrl = await createPostgresDb('json-field-typing');
+ });
+
+ afterEach(async () => {
+ if (prisma) {
+ await prisma.$disconnect();
+ }
+ await dropPostgresDb(dbUrl);
+ });
+
+ it('works with simple cases', async () => {
+ const params = await loadSchema(
+ `
+ type Address {
+ city String
+ }
+
+ type Profile {
+ age Int
+ address Address?
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ posts Post[]
+ }
+
+ model Post {
+ id Int @id @default(autoincrement())
+ title String
+ user User @relation(fields: [userId], references: [id])
+ userId Int
+ }
+ `,
+ {
+ provider: 'postgresql',
+ dbUrl,
+ enhancements: ['validation'],
+ }
+ );
+
+ prisma = params.prisma;
+ const db = params.enhance();
+
+ // expecting object
+ await expect(db.user.create({ data: { profile: 1 } })).toBeRejectedByPolicy();
+ await expect(db.user.create({ data: { profile: [{ age: 18 }] } })).toBeRejectedByPolicy();
+ await expect(db.user.create({ data: { profile: { myAge: 18 } } })).toBeRejectedByPolicy();
+ await expect(db.user.create({ data: { profile: { address: { city: 'NY' } } } })).toBeRejectedByPolicy();
+ await expect(db.user.create({ data: { profile: { age: 18, address: { x: 1 } } } })).toBeRejectedByPolicy();
+
+ await expect(
+ db.user.create({ data: { profile: { age: 18 }, posts: { create: { title: 'Post1' } } } })
+ ).resolves.toMatchObject({
+ profile: { age: 18 },
+ });
+ await expect(
+ db.user.create({
+ data: { profile: { age: 20, address: { city: 'NY' } }, posts: { create: { title: 'Post1' } } },
+ })
+ ).resolves.toMatchObject({
+ profile: { age: 20, address: { city: 'NY' } },
+ });
+ });
+
+ it('works with array', async () => {
+ const params = await loadSchema(
+ `
+ type Address {
+ city String
+ }
+
+ type Profile {
+ age Int
+ address Address?
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profiles Profile[] @json
+ @@allow('all', true)
+ }
+ `,
+ {
+ provider: 'postgresql',
+ dbUrl,
+ }
+ );
+
+ prisma = params.prisma;
+ const db = params.enhance();
+
+ // expecting array
+ await expect(
+ db.user.create({ data: { profiles: { age: 18, address: { city: 'NY' } } } })
+ ).toBeRejectedByPolicy();
+
+ await expect(
+ db.user.create({ data: { profiles: [{ age: 18, address: { city: 'NY' } }] } })
+ ).resolves.toMatchObject({
+ profiles: expect.arrayContaining([expect.objectContaining({ age: 18, address: { city: 'NY' } })]),
+ });
+ });
+
+ it('respects validation rules', async () => {
+ const params = await loadSchema(
+ `
+ type Address {
+ city String @length(2, 10)
+ }
+
+ type Profile {
+ age Int @gte(18)
+ address Address?
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ foo Foo?
+ @@allow('all', true)
+ }
+
+ model Foo {
+ id Int @id @default(autoincrement())
+ user User @relation(fields: [userId], references: [id])
+ userId Int @unique
+ @@allow('all', true)
+ }
+ `,
+ {
+ provider: 'postgresql',
+ dbUrl,
+ }
+ );
+
+ prisma = params.prisma;
+ const db = params.enhance();
+
+ // create
+ await expect(db.user.create({ data: { profile: { age: 10 } } })).toBeRejectedByPolicy();
+ await expect(db.user.create({ data: { profile: { age: 18, address: { city: 'N' } } } })).toBeRejectedByPolicy();
+ const u1 = await db.user.create({ data: { profile: { age: 18, address: { city: 'NY' } } } });
+ expect(u1).toMatchObject({
+ profile: { age: 18, address: { city: 'NY' } },
+ });
+
+ // update
+ await expect(db.user.update({ where: { id: u1.id }, data: { profile: { age: 10 } } })).toBeRejectedByPolicy();
+ await expect(
+ db.user.update({ where: { id: u1.id }, data: { profile: { age: 20, address: { city: 'B' } } } })
+ ).toBeRejectedByPolicy();
+ await expect(
+ db.user.update({ where: { id: u1.id }, data: { profile: { age: 20, address: { city: 'BJ' } } } })
+ ).resolves.toMatchObject({
+ profile: { age: 20, address: { city: 'BJ' } },
+ });
+
+ // nested create
+ await expect(db.foo.create({ data: { user: { create: { profile: { age: 10 } } } } })).toBeRejectedByPolicy();
+ await expect(db.foo.create({ data: { user: { create: { profile: { age: 20 } } } } })).toResolveTruthy();
+
+ // upsert
+ await expect(
+ db.user.upsert({ where: { id: 10 }, create: { id: 10, profile: { age: 10 } }, update: {} })
+ ).toBeRejectedByPolicy();
+ await expect(
+ db.user.upsert({ where: { id: 10 }, create: { id: 10, profile: { age: 20 } }, update: {} })
+ ).toResolveTruthy();
+ await expect(
+ db.user.upsert({
+ where: { id: 10 },
+ create: { id: 10, profile: { age: 20 } },
+ update: { profile: { age: 10 } },
+ })
+ ).toBeRejectedByPolicy();
+ await expect(
+ db.user.upsert({
+ where: { id: 10 },
+ create: { id: 10, profile: { age: 20 } },
+ update: { profile: { age: 20 } },
+ })
+ ).toResolveTruthy();
+ });
+
+ it('respects @default', async () => {
+ const params = await loadSchema(
+ `
+ type Address {
+ state String
+ city String @default('Issaquah')
+ }
+
+ type Profile {
+ createdAt DateTime @default(now())
+ address Address?
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ @@allow('all', true)
+ }
+ `,
+ {
+ provider: 'postgresql',
+ dbUrl,
+ }
+ );
+
+ prisma = params.prisma;
+ const db = params.enhance();
+
+ // default value
+ await expect(db.user.create({ data: { profile: { address: { state: 'WA' } } } })).resolves.toMatchObject({
+ profile: { address: { state: 'WA', city: 'Issaquah' }, createdAt: expect.any(Date) },
+ });
+
+ // override default
+ await expect(
+ db.user.create({ data: { profile: { address: { state: 'WA', city: 'Seattle' } } } })
+ ).resolves.toMatchObject({
+ profile: { address: { state: 'WA', city: 'Seattle' } },
+ });
+ });
+
+ it('works auth() in @default', async () => {
+ const params = await loadSchema(
+ `
+ type NestedProfile {
+ userId Int @default(auth().id)
+ }
+
+ type Profile {
+ ownerId Int @default(auth().id)
+ nested NestedProfile
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ @@allow('all', true)
+ }
+ `,
+ {
+ provider: 'postgresql',
+ dbUrl,
+ }
+ );
+
+ prisma = params.prisma;
+
+ const db = params.enhance({ id: 1 });
+ const u1 = await db.user.create({ data: { profile: { nested: {} } } });
+ expect(u1.profile.ownerId).toBe(1);
+ expect(u1.profile.nested.userId).toBe(1);
+
+ const u2 = await db.user.create({ data: { profile: { ownerId: 2, nested: { userId: 3 } } } });
+ expect(u2.profile.ownerId).toBe(2);
+ expect(u2.profile.nested.userId).toBe(3);
+ });
+});
diff --git a/tests/integration/tests/enhancements/json/typing.test.ts b/tests/integration/tests/enhancements/json/typing.test.ts
new file mode 100644
index 000000000..9681bf015
--- /dev/null
+++ b/tests/integration/tests/enhancements/json/typing.test.ts
@@ -0,0 +1,233 @@
+import { loadSchema } from '@zenstackhq/testtools';
+
+describe('JSON field typing', () => {
+ it('works with simple field', async () => {
+ await loadSchema(
+ `
+ type Profile {
+ age Int @gt(0)
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ posts Post[]
+ @@allow('all', true)
+ }
+
+ model Post {
+ id Int @id @default(autoincrement())
+ title String
+ user User @relation(fields: [userId], references: [id])
+ userId Int
+ }
+ `,
+ {
+ provider: 'postgresql',
+ pushDb: false,
+ compile: true,
+ extraSourceFiles: [
+ {
+ name: 'main.ts',
+ content: `
+import { enhance } from '.zenstack/enhance';
+import { PrismaClient } from '@prisma/client';
+const prisma = new PrismaClient();
+const db = enhance(prisma);
+
+async function main() {
+ const u = await db.user.create({ data: { profile: { age: 18 }, posts: { create: { title: 'Post1' }} } });
+ console.log(u.profile.age);
+ const u1 = await db.user.findUnique({ where: { id: u.id } });
+ console.log(u1?.profile.age);
+ const u2 = await db.user.findMany({include: { posts: true }});
+ console.log(u2[0].profile.age);
+}
+ `,
+ },
+ ],
+ }
+ );
+ });
+
+ it('works with optional field', async () => {
+ await loadSchema(
+ `
+ type Profile {
+ age Int @gt(0)
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile? @json
+ @@allow('all', true)
+ }
+ `,
+ {
+ provider: 'postgresql',
+ pushDb: false,
+ compile: true,
+ extraSourceFiles: [
+ {
+ name: 'main.ts',
+ content: `
+import { enhance } from '.zenstack/enhance';
+import { PrismaClient } from '@prisma/client';
+const prisma = new PrismaClient();
+const db = enhance(prisma);
+
+async function main() {
+ const u = await db.user.create({ data: { profile: { age: 18 } } });
+ console.log(u.profile?.age);
+ const u1 = await db.user.findUnique({ where: { id: u.id } });
+ console.log(u1?.profile?.age);
+ const u2 = await db.user.findMany();
+ console.log(u2[0].profile?.age);
+}
+ `,
+ },
+ ],
+ }
+ );
+ });
+
+ it('works with array field', async () => {
+ await loadSchema(
+ `
+ type Profile {
+ age Int @gt(0)
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profiles Profile[] @json
+ @@allow('all', true)
+ }
+ `,
+ {
+ provider: 'postgresql',
+ pushDb: false,
+ compile: true,
+ extraSourceFiles: [
+ {
+ name: 'main.ts',
+ content: `
+import { enhance } from '.zenstack/enhance';
+import { PrismaClient } from '@prisma/client';
+const prisma = new PrismaClient();
+const db = enhance(prisma);
+
+async function main() {
+ const u = await db.user.create({ data: { profiles: [{ age: 18 }] } });
+ console.log(u.profiles[0].age);
+ const u1 = await db.user.findUnique({ where: { id: u.id } });
+ console.log(u1?.profiles[0].age);
+ const u2 = await db.user.findMany();
+ console.log(u2[0].profiles[0].age);
+}
+ `,
+ },
+ ],
+ }
+ );
+ });
+
+ it('works with type nesting', async () => {
+ await loadSchema(
+ `
+ type Profile {
+ age Int @gt(0)
+ address Address?
+ }
+
+ type Address {
+ city String
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ @@allow('all', true)
+ }
+ `,
+ {
+ provider: 'postgresql',
+ pushDb: false,
+ compile: true,
+ extraSourceFiles: [
+ {
+ name: 'main.ts',
+ content: `
+import { enhance } from '.zenstack/enhance';
+import { PrismaClient } from '@prisma/client';
+const prisma = new PrismaClient();
+const db = enhance(prisma);
+
+async function main() {
+ const u = await db.user.create({ data: { profile: { age: 18, address: { city: 'Issaquah' } } } });
+ console.log(u.profile.address?.city);
+ const u1 = await db.user.findUnique({ where: { id: u.id } });
+ console.log(u1?.profile.address?.city);
+ const u2 = await db.user.findMany();
+ console.log(u2[0].profile.address?.city);
+ await db.user.create({ data: { profile: { age: 20 } } });
+}
+ `,
+ },
+ ],
+ }
+ );
+ });
+
+ it('type coverage', async () => {
+ await loadSchema(
+ `
+ type Profile {
+ boolean Boolean
+ bigint BigInt
+ int Int
+ float Float
+ decimal Decimal
+ string String
+ bytes Bytes
+ dateTime DateTime
+ json Json
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ @@allow('all', true)
+ }
+ `,
+ {
+ provider: 'postgresql',
+ pushDb: false,
+ compile: true,
+ extraSourceFiles: [
+ {
+ name: 'main.ts',
+ content: `
+import type { Profile } from '.zenstack/models';
+import { Prisma } from '@prisma/client';
+
+async function main() {
+ const profile: Profile = {
+ boolean: true,
+ bigint: BigInt(9007199254740991),
+ int: 100,
+ float: 1.23,
+ decimal: new Prisma.Decimal(1.2345),
+ string: 'string',
+ bytes: new Uint8Array([0, 1, 2, 3]),
+ dateTime: new Date(),
+ json: { a: 1 },
+ }
+}
+ `,
+ },
+ ],
+ }
+ );
+ });
+});
diff --git a/tests/integration/tests/enhancements/json/validation.test.ts b/tests/integration/tests/enhancements/json/validation.test.ts
new file mode 100644
index 000000000..2643056fe
--- /dev/null
+++ b/tests/integration/tests/enhancements/json/validation.test.ts
@@ -0,0 +1,39 @@
+import { loadModelWithError } from '@zenstackhq/testtools';
+
+describe('JSON field typing', () => {
+ it('is only supported by postgres', async () => {
+ await expect(
+ loadModelWithError(
+ `
+ type Profile {
+ age Int @gt(0)
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ @@allow('all', true)
+ }
+ `
+ )
+ ).resolves.toContain('Custom-typed field is only supported with "postgresql" provider');
+ });
+
+ it('requires field to have @json attribute', async () => {
+ await expect(
+ loadModelWithError(
+ `
+ type Profile {
+ age Int @gt(0)
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile
+ @@allow('all', true)
+ }
+ `
+ )
+ ).resolves.toContain('Custom-typed field must have @json attribute');
+ });
+});
diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts
index 59a3f68c0..91a385db0 100644
--- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts
+++ b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts
@@ -1407,4 +1407,101 @@ describe('Polymorphism Test', () => {
r = await db.post.findFirst({ include: { comments: true } });
expect(r).toMatchObject({ ...post, comments: [comment] });
});
+
+ it('works with one-to-one self relation', async () => {
+ const { enhance } = await loadSchema(
+ `
+ model User {
+ id Int @id @default(autoincrement())
+ successorId Int? @unique
+ successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])
+ predecessor User? @relation("BlogOwnerHistory")
+ type String
+ @@delegate(type)
+ }
+
+ model Person extends User {
+ }
+
+ model Organization extends User {
+ }
+ `,
+ { enhancements: ['delegate'] }
+ );
+
+ const db = enhance();
+ const u1 = await db.person.create({ data: {} });
+ const u2 = await db.organization.create({
+ data: { predecessor: { connect: { id: u1.id } } },
+ include: { predecessor: true },
+ });
+ expect(u2).toMatchObject({ id: u2.id, predecessor: { id: u1.id } });
+ const foundP1 = await db.person.findUnique({ where: { id: u1.id }, include: { successor: true } });
+ expect(foundP1).toMatchObject({ id: u1.id, successor: { id: u2.id } });
+ });
+
+ it('works with one-to-many self relation', async () => {
+ const { enhance } = await loadSchema(
+ `
+ model User {
+ id Int @id @default(autoincrement())
+ name String?
+ parentId Int?
+ parent User? @relation("ParentChild", fields: [parentId], references: [id])
+ children User[] @relation("ParentChild")
+ type String
+ @@delegate(type)
+ }
+
+ model Person extends User {
+ }
+
+ model Organization extends User {
+ }
+ `,
+ { enhancements: ['delegate'] }
+ );
+
+ const db = enhance();
+ const u1 = await db.person.create({ data: {} });
+ const u2 = await db.organization.create({
+ data: { parent: { connect: { id: u1.id } } },
+ include: { parent: true },
+ });
+ expect(u2).toMatchObject({ id: u2.id, parent: { id: u1.id } });
+ const foundP1 = await db.person.findUnique({ where: { id: u1.id }, include: { children: true } });
+ expect(foundP1).toMatchObject({ id: u1.id, children: [{ id: u2.id }] });
+ });
+
+ it('works with many-to-many self relation', async () => {
+ const { enhance } = await loadSchema(
+ `
+ model User {
+ id Int @id @default(autoincrement())
+ name String?
+ followedBy User[] @relation("UserFollows")
+ following User[] @relation("UserFollows")
+ type String
+ @@delegate(type)
+ }
+
+ model Person extends User {
+ }
+
+ model Organization extends User {
+ }
+ `,
+ { enhancements: ['delegate'] }
+ );
+
+ const db = enhance();
+ const u1 = await db.person.create({ data: {} });
+ const u2 = await db.organization.create({
+ data: { following: { connect: { id: u1.id } } },
+ include: { following: true },
+ });
+ expect(u2).toMatchObject({ id: u2.id, following: [{ id: u1.id }] });
+ const foundP1 = await db.person.findUnique({ where: { id: u1.id }, include: { followedBy: true } });
+ expect(foundP1).toMatchObject({ id: u1.id, followedBy: [{ id: u2.id }] });
+ });
});
diff --git a/tests/integration/tests/enhancements/with-policy/checker.test.ts b/tests/integration/tests/enhancements/with-policy/checker.test.ts
index e4ca61fad..a109c3ef6 100644
--- a/tests/integration/tests/enhancements/with-policy/checker.test.ts
+++ b/tests/integration/tests/enhancements/with-policy/checker.test.ts
@@ -357,7 +357,7 @@ describe('Permission checker', () => {
await expect(db.model.check({ operation: 'update', where: { x: 1, y: 1 } })).toResolveFalsy();
});
- it('field condition unsolvable', async () => {
+ it('field condition unsatisfiable', async () => {
const { enhance } = await load(
`
model Model {
@@ -649,4 +649,115 @@ describe('Permission checker', () => {
await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy();
await expect(db.model.check({ operation: 'read', where: { value: 2 } })).toResolveTruthy();
});
+
+ it('supports policy delegation simple', async () => {
+ const { enhance } = await load(
+ `
+ model User {
+ id Int @id @default(autoincrement())
+ foo Foo[]
+ }
+
+ model Foo {
+ id Int @id @default(autoincrement())
+ owner User @relation(fields: [ownerId], references: [id])
+ ownerId Int
+ model Model?
+ @@allow('read', auth().id == ownerId)
+ @@allow('create', auth().id != ownerId)
+ @@allow('update', auth() == owner)
+ }
+
+ model Model {
+ id Int @id @default(autoincrement())
+ foo Foo @relation(fields: [fooId], references: [id])
+ fooId Int @unique
+ @@allow('all', check(foo))
+ }
+ `,
+ { preserveTsFiles: true }
+ );
+
+ await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'read' })).toResolveTruthy();
+
+ await expect(enhance().model.check({ operation: 'create' })).toResolveFalsy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'create' })).toResolveTruthy();
+
+ await expect(enhance().model.check({ operation: 'update' })).toResolveFalsy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'update' })).toResolveTruthy();
+
+ await expect(enhance().model.check({ operation: 'delete' })).toResolveFalsy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'delete' })).toResolveFalsy();
+ });
+
+ it('supports policy delegation explicit', async () => {
+ const { enhance } = await load(
+ `
+ model Foo {
+ id Int @id @default(autoincrement())
+ model Model?
+ @@allow('all', true)
+ @@deny('update', true)
+ }
+
+ model Model {
+ id Int @id @default(autoincrement())
+ foo Foo @relation(fields: [fooId], references: [id])
+ fooId Int @unique
+ @@allow('read', check(foo, 'update'))
+ }
+ `,
+ { preserveTsFiles: true }
+ );
+
+ await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy();
+ });
+
+ it('supports policy delegation combined', async () => {
+ const { enhance } = await load(
+ `
+ model User {
+ id Int @id @default(autoincrement())
+ foo Foo[]
+ }
+
+ model Foo {
+ id Int @id @default(autoincrement())
+ owner User @relation(fields: [ownerId], references: [id])
+ ownerId Int
+ model Model?
+ @@allow('read', auth().id == ownerId)
+ @@allow('create', auth().id != ownerId)
+ @@allow('update', auth() == owner)
+ }
+
+ model Model {
+ id Int @id @default(autoincrement())
+ foo Foo @relation(fields: [fooId], references: [id])
+ fooId Int @unique
+ value Int
+ @@allow('all', check(foo) && value > 0)
+ @@deny('update', check(foo) && value == 1)
+ }
+ `,
+ { preserveTsFiles: true }
+ );
+
+ await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'read' })).toResolveTruthy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'read', where: { value: 0 } })).toResolveFalsy();
+
+ await expect(enhance().model.check({ operation: 'create' })).toResolveFalsy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'create' })).toResolveTruthy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'create', where: { value: 1 } })).toResolveTruthy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'create', where: { value: 0 } })).toResolveFalsy();
+
+ await expect(enhance().model.check({ operation: 'update' })).toResolveFalsy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'update' })).toResolveTruthy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'update', where: { value: 2 } })).toResolveTruthy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'update', where: { value: 0 } })).toResolveFalsy();
+ await expect(enhance({ id: 1 }).model.check({ operation: 'update', where: { value: 1 } })).toResolveFalsy();
+ });
});
diff --git a/tests/integration/tests/frameworks/nextjs/test-project/package.json b/tests/integration/tests/frameworks/nextjs/test-project/package.json
index 8ead9a366..95a3bac9e 100644
--- a/tests/integration/tests/frameworks/nextjs/test-project/package.json
+++ b/tests/integration/tests/frameworks/nextjs/test-project/package.json
@@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
- "@prisma/client": "5.21.x",
+ "@prisma/client": "5.22.x",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
@@ -26,6 +26,6 @@
"@zenstackhq/swr": "../../../../../../../packages/plugins/swr/dist"
},
"devDependencies": {
- "prisma": "5.21.x"
+ "prisma": "5.22.x"
}
}
diff --git a/tests/integration/tests/frameworks/trpc/test-project/package.json b/tests/integration/tests/frameworks/trpc/test-project/package.json
index a23a84e24..676f3f165 100644
--- a/tests/integration/tests/frameworks/trpc/test-project/package.json
+++ b/tests/integration/tests/frameworks/trpc/test-project/package.json
@@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
- "@prisma/client": "5.21.x",
+ "@prisma/client": "5.22.x",
"@tanstack/react-query": "^4.22.4",
"@trpc/client": "^10.34.0",
"@trpc/next": "^10.34.0",
@@ -31,6 +31,6 @@
"@zenstackhq/trpc": "../../../../../../../packages/plugins/trpc/dist"
},
"devDependencies": {
- "prisma": "5.21.x"
+ "prisma": "5.22.x"
}
}
diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts
index 5af7f4077..aba94261c 100644
--- a/tests/integration/tests/plugins/zod.test.ts
+++ b/tests/integration/tests/plugins/zod.test.ts
@@ -1025,4 +1025,60 @@ describe('Zod plugin tests', () => {
)
).rejects.toThrow(/Invalid mode/);
});
+
+ it('supports type def', async () => {
+ const { zodSchemas } = await loadSchema(
+ `
+ datasource db {
+ provider = 'postgresql'
+ url = env('DATABASE_URL')
+ }
+
+ generator js {
+ provider = 'prisma-client-js'
+ }
+
+ plugin zod {
+ provider = '@core/zod'
+ }
+
+ type Address {
+ city String @length(2, 20)
+ }
+
+ type Profile {
+ age Int @gte(18)
+ address Address?
+ }
+
+ model User {
+ id Int @id @default(autoincrement())
+ profile Profile @json
+ }
+ `,
+ { addPrelude: false, pushDb: false }
+ );
+
+ const schemas = zodSchemas.models;
+
+ let parsed = schemas.ProfileSchema.safeParse({ age: 18, address: { city: 'NY' } });
+ expect(parsed.success).toBeTruthy();
+ expect(parsed.data).toEqual({ age: 18, address: { city: 'NY' } });
+
+ expect(schemas.ProfileSchema.safeParse({ age: 18 })).toMatchObject({ success: true });
+ expect(schemas.ProfileSchema.safeParse({ age: 10 })).toMatchObject({ success: false });
+ expect(schemas.ProfileSchema.safeParse({ address: { city: 'NY' } })).toMatchObject({ success: false });
+ expect(schemas.ProfileSchema.safeParse({ address: { age: 18, city: 'N' } })).toMatchObject({ success: false });
+
+ expect(schemas.UserSchema.safeParse({ id: 1, profile: { age: 18 } })).toMatchObject({ success: true });
+ expect(schemas.UserSchema.safeParse({ id: 1, profile: { age: 10 } })).toMatchObject({ success: false });
+
+ const objectSchemas = zodSchemas.objects;
+ expect(objectSchemas.UserCreateInputObjectSchema.safeParse({ profile: { age: 18 } })).toMatchObject({
+ success: true,
+ });
+ expect(objectSchemas.UserCreateInputObjectSchema.safeParse({ profile: { age: 10 } })).toMatchObject({
+ success: false,
+ });
+ });
});
diff --git a/tests/regression/tests/issue-1755.test.ts b/tests/regression/tests/issue-1755.test.ts
new file mode 100644
index 000000000..9e41f7c6e
--- /dev/null
+++ b/tests/regression/tests/issue-1755.test.ts
@@ -0,0 +1,61 @@
+import { loadSchema } from '@zenstackhq/testtools';
+
+describe('issue 1755', () => {
+ it('regression', async () => {
+ const { enhance } = await loadSchema(
+ `
+ model User {
+ id Int @id @default(autoincrement())
+ contents Content[]
+ }
+
+ model Content {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ user User @relation(fields: [userId], references: [id])
+ userId Int
+ contentType String
+ @@delegate(contentType)
+ }
+
+ model Post extends Content {
+ title String
+ }
+
+ model Video extends Content {
+ name String
+ duration Int
+ }
+ `,
+ { enhancements: ['delegate'] }
+ );
+
+ const db = enhance();
+ const user = await db.user.create({ data: {} });
+ const now = Date.now();
+ await db.post.create({
+ data: { title: 'post1', createdAt: new Date(now - 1000), user: { connect: { id: user.id } } },
+ });
+ await db.post.create({
+ data: { title: 'post2', createdAt: new Date(now), user: { connect: { id: user.id } } },
+ });
+
+ // scalar orderBy
+ await expect(db.post.findFirst({ orderBy: { createdAt: 'desc' } })).resolves.toMatchObject({
+ title: 'post2',
+ });
+
+ // array orderBy
+ await expect(db.post.findFirst({ orderBy: [{ createdAt: 'desc' }] })).resolves.toMatchObject({
+ title: 'post2',
+ });
+
+ // nested orderBy
+ await expect(
+ db.user.findFirst({ include: { contents: { orderBy: [{ createdAt: 'desc' }] } } })
+ ).resolves.toMatchObject({
+ id: user.id,
+ contents: [{ title: 'post2' }, { title: 'post1' }],
+ });
+ });
+});
diff --git a/tests/regression/tests/issue-1786.test.ts b/tests/regression/tests/issue-1786.test.ts
new file mode 100644
index 000000000..ae37297de
--- /dev/null
+++ b/tests/regression/tests/issue-1786.test.ts
@@ -0,0 +1,48 @@
+import { loadSchema } from '@zenstackhq/testtools';
+
+describe('issue 1786', () => {
+ it('regression', async () => {
+ await loadSchema(
+ `
+ model User {
+ id String @id @default(cuid())
+ email String @unique @email @length(6, 32)
+ password String @password @omit
+ contents Content[]
+
+ // everybody can signup
+ @@allow('create', true)
+
+ // full access by self
+ @@allow('all', auth() == this)
+ }
+
+ abstract model BaseContent {
+ published Boolean @default(false)
+
+ @@index([published])
+ }
+
+ model Content extends BaseContent {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ owner User @relation(fields: [ownerId], references: [id])
+ ownerId String
+ contentType String
+
+ @@delegate(contentType)
+ }
+
+ model Post extends Content {
+ title String
+ }
+
+ model Video extends Content {
+ name String
+ duration Int
+ }
+ `
+ );
+ });
+});