From fac631b7a3b6e04cbfd1e0c615f0336783065e51 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Wed, 23 Oct 2024 21:56:10 -0700
Subject: [PATCH 01/20] fix(delegate): discriminator fields should be removed
 from unchecked create/update input types (#1804)

---
 .../src/plugins/enhancer/enhance/index.ts     | 23 +++++----
 tests/regression/tests/issue-1763.test.ts     | 47 +++++++++++++++++++
 2 files changed, 60 insertions(+), 10 deletions(-)
 create mode 100644 tests/regression/tests/issue-1763.test.ts

diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts
index ff2e00b6a..34cf26640 100644
--- a/packages/schema/src/plugins/enhancer/enhance/index.ts
+++ b/packages/schema/src/plugins/enhancer/enhance/index.ts
@@ -515,15 +515,11 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara
         return source;
     }
 
-    private removeCreateFromDelegateInput(
-        typeAlias: TypeAliasDeclaration,
-        delegateModels: DelegateInfo,
-        source: string
-    ) {
+    private removeCreateFromDelegateInput(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo, source: string) {
         // remove create/connectOrCreate/upsert fields from delegate's input types because
         // delegate models cannot be created directly
         const typeName = typeAlias.getName();
-        const delegateModelNames = delegateModels.map(([delegate]) => delegate.name);
+        const delegateModelNames = delegateInfo.map(([delegate]) => delegate.name);
         const delegateCreateUpdateInputRegex = new RegExp(
             `^(${delegateModelNames.join('|')})(Unchecked)?(Create|Update).*Input$`
         );
@@ -538,17 +534,24 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara
         return source;
     }
 
-    private readonly ModelCreateUpdateInputRegex = /(\S+)(Unchecked)?(Create|Update).*Input/;
-
     private removeDiscriminatorFromConcreteInput(
         typeAlias: TypeAliasDeclaration,
-        _delegateInfo: DelegateInfo,
+        delegateInfo: DelegateInfo,
         source: string
     ) {
         // remove discriminator field from the create/update input because discriminator cannot be set directly
         const typeName = typeAlias.getName();
 
-        const match = typeName.match(this.ModelCreateUpdateInputRegex);
+        const delegateModelNames = delegateInfo.map(([delegate]) => delegate.name);
+        const concreteModelNames = delegateInfo
+            .map(([_, concretes]) => concretes.flatMap((c) => c.name))
+            .flatMap((name) => name);
+        const allModelNames = [...new Set([...delegateModelNames, ...concreteModelNames])];
+        const concreteCreateUpdateInputRegex = new RegExp(
+            `^(${allModelNames.join('|')})(Unchecked)?(Create|Update).*Input$`
+        );
+
+        const match = typeName.match(concreteCreateUpdateInputRegex);
         if (match) {
             const modelName = match[1];
             const dataModel = this.model.declarations.find(
diff --git a/tests/regression/tests/issue-1763.test.ts b/tests/regression/tests/issue-1763.test.ts
new file mode 100644
index 000000000..d5ea1d401
--- /dev/null
+++ b/tests/regression/tests/issue-1763.test.ts
@@ -0,0 +1,47 @@
+import { loadSchema } from '@zenstackhq/testtools';
+
+describe('issue 1763', () => {
+    it('regression', async () => {
+        await loadSchema(
+            `
+            model Post {
+                id   Int    @id @default(autoincrement())
+                name String
+
+                type String
+                @@delegate(type)
+
+                // full access by author
+                @@allow('all', true)
+            }
+
+            model ConcretePost extends Post {
+                age Int
+            }
+            `,
+            {
+                compile: true,
+                extraSourceFiles: [
+                    {
+                        name: 'main.ts',
+                        content: `
+import { PrismaClient as Prisma } from '@prisma/client';
+import { enhance } from '@zenstackhq/runtime';
+
+async function test() {
+    const prisma = new Prisma();
+    const db = enhance(prisma);
+    await db.concretePost.create({
+        data: {
+            id: 5,
+            name: 'a name',
+            age: 20,
+        },
+    });
+}                        `,
+                    },
+                ],
+            }
+        );
+    });
+});

From b8c84a5d1a2f20fe0c487150a5ac012cb052583a Mon Sep 17 00:00:00 2001
From: Amruth Pillai <im.amruth@gmail.com>
Date: Thu, 24 Oct 2024 19:54:45 +0200
Subject: [PATCH 02/20] =?UTF-8?q?=F0=9F=90=9E=20fix=20(server):=20support?=
 =?UTF-8?q?=20for=20awaiting=20next.js=2015=20params=20(#1805)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/server/src/next/app-route-handler.ts | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/packages/server/src/next/app-route-handler.ts b/packages/server/src/next/app-route-handler.ts
index 5c8cbe0e5..8347c5eb0 100644
--- a/packages/server/src/next/app-route-handler.ts
+++ b/packages/server/src/next/app-route-handler.ts
@@ -6,11 +6,12 @@ import { AppRouteRequestHandlerOptions } from '.';
 import { RPCApiHandler } from '../api';
 import { loadAssets } from '../shared';
 
-type Context = { params: { path: string[] } };
+type Context = { params: Promise<{ path: string[] }> | { path: string[] } };
 
 /**
  * Creates a Next.js 13 "app dir" API route request handler which encapsulates Prisma CRUD operations.
  *
+ * @remarks Since Next.js 15, `context.params` is asynchronous and must be awaited.
  * @param options Options for initialization
  * @returns An API route request handler
  */
@@ -27,10 +28,17 @@ export default function factory(
             return NextResponse.json({ message: 'unable to get prisma from request context' }, { status: 500 });
         }
 
+        let params: Context['params'];
         const url = new URL(req.url);
         const query = Object.fromEntries(url.searchParams);
 
-        if (!context.params.path) {
+        try {
+            params = await context.params;
+        } catch {
+            return NextResponse.json({ message: 'Failed to resolve request parameters' }, { status: 500 });
+        }
+
+        if (!params.path) {
             return NextResponse.json(
                 { message: 'missing path parameter' },
                 {
@@ -38,7 +46,7 @@ export default function factory(
                 }
             );
         }
-        const path = context.params.path.join('/');
+        const path = params.path.join('/');
 
         let requestBody: unknown;
         if (req.body) {

From efc9da2d944ba8b08032795e70d8a9fc5747ba08 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Thu, 24 Oct 2024 20:53:46 -0700
Subject: [PATCH 03/20] chore: bump version (#1807)

---
 package.json                                 | 2 +-
 packages/ide/jetbrains/build.gradle.kts      | 2 +-
 packages/ide/jetbrains/package.json          | 2 +-
 packages/language/package.json               | 2 +-
 packages/misc/redwood/package.json           | 2 +-
 packages/plugins/openapi/package.json        | 2 +-
 packages/plugins/swr/package.json            | 2 +-
 packages/plugins/tanstack-query/package.json | 2 +-
 packages/plugins/trpc/package.json           | 2 +-
 packages/runtime/package.json                | 2 +-
 packages/schema/package.json                 | 2 +-
 packages/sdk/package.json                    | 2 +-
 packages/server/package.json                 | 2 +-
 packages/testtools/package.json              | 2 +-
 14 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/package.json b/package.json
index a392b07ee..94237146e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "zenstack-monorepo",
-    "version": "2.7.3",
+    "version": "2.7.4",
     "description": "",
     "scripts": {
         "build": "pnpm -r build",
diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts
index 5ab74e6ad..a2e6892fe 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.3"
+version = "2.7.4"
 
 repositories {
     mavenCentral()
diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json
index 805f1d2d6..5a43c79e0 100644
--- a/packages/ide/jetbrains/package.json
+++ b/packages/ide/jetbrains/package.json
@@ -1,6 +1,6 @@
 {
   "name": "jetbrains",
-  "version": "2.7.3",
+  "version": "2.7.4",
   "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 738f2872f..f5b30d6e0 100644
--- a/packages/language/package.json
+++ b/packages/language/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@zenstackhq/language",
-    "version": "2.7.3",
+    "version": "2.7.4",
     "displayName": "ZenStack modeling language compiler",
     "description": "ZenStack modeling language compiler",
     "homepage": "https://zenstack.dev",
diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json
index a37e9a403..752d79017 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.3",
+    "version": "2.7.4",
     "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 a34343885..ed83d25b2 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.3",
+  "version": "2.7.4",
   "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 fb9ab1c8d..42de62697 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.3",
+    "version": "2.7.4",
     "description": "ZenStack plugin for generating SWR hooks",
     "main": "index.js",
     "repository": {
diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json
index 6ce063400..05a97815c 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.3",
+    "version": "2.7.4",
     "description": "ZenStack plugin for generating tanstack-query hooks",
     "main": "index.js",
     "exports": {
diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json
index 069e68497..0e7f4afe5 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.3",
+    "version": "2.7.4",
     "description": "ZenStack plugin for tRPC",
     "main": "index.js",
     "repository": {
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 880f4f809..088bde289 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.3",
+    "version": "2.7.4",
     "description": "Runtime of ZenStack for both client-side and server-side environments.",
     "repository": {
         "type": "git",
diff --git a/packages/schema/package.json b/packages/schema/package.json
index 467712d8b..efb704ba1 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.3",
+    "version": "2.7.4",
     "author": {
         "name": "ZenStack Team"
     },
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 72de665bc..ea62b7d44 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@zenstackhq/sdk",
-    "version": "2.7.3",
+    "version": "2.7.4",
     "description": "ZenStack plugin development SDK",
     "main": "index.js",
     "scripts": {
diff --git a/packages/server/package.json b/packages/server/package.json
index 137d42c0c..49d897cef 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@zenstackhq/server",
-    "version": "2.7.3",
+    "version": "2.7.4",
     "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 ab6db5bee..ad909bdb7 100644
--- a/packages/testtools/package.json
+++ b/packages/testtools/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@zenstackhq/testtools",
-  "version": "2.7.3",
+  "version": "2.7.4",
   "description": "ZenStack Test Tools",
   "main": "index.js",
   "private": true,

From c344f7794180d9be4366ab06853d60dd450547f5 Mon Sep 17 00:00:00 2001
From: Thomas Sunde Nielsen <me@thomassnielsen.com>
Date: Fri, 25 Oct 2024 18:04:49 +0200
Subject: [PATCH 04/20] Improve working with entities related to entities that
 uses compound ids (#1801)

Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com>
---
 .../plugins/openapi/src/rest-generator.ts     |  6 ++--
 .../tests/baseline/rest-3.0.0.baseline.yaml   | 15 --------
 .../tests/baseline/rest-3.1.0.baseline.yaml   | 17 ---------
 packages/schema/src/plugins/zod/generator.ts  |  5 ++-
 packages/server/src/api/rest/index.ts         | 36 +++++++++++++------
 packages/server/tests/api/rest.test.ts        | 35 ++++++++++++++++++
 6 files changed, 68 insertions(+), 46 deletions(-)

diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts
index 7cf465d9e..8927198cc 100644
--- a/packages/plugins/openapi/src/rest-generator.ts
+++ b/packages/plugins/openapi/src/rest-generator.ts
@@ -847,8 +847,10 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
 
     private generateModelEntity(model: DataModel, mode: 'read' | 'create' | 'update'): OAPI.SchemaObject {
         const idFields = model.fields.filter((f) => isIdField(f));
-        // For compound ids, each component is also exposed as a separate field
-        const fields = idFields.length > 1 ? model.fields : model.fields.filter((f) => !isIdField(f));
+        // For compound ids each component is also exposed as a separate fields for read operations,
+        // but not required for write operations
+        const fields =
+            idFields.length > 1 && mode === 'read' ? model.fields : model.fields.filter((f) => !isIdField(f));
 
         const attributes: Record<string, OAPI.SchemaObject> = {};
         const relationships: Record<string, OAPI.ReferenceObject | OAPI.SchemaObject> = {};
diff --git a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml
index 96f80d81a..adb9ded12 100644
--- a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml
+++ b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml
@@ -3143,14 +3143,6 @@ components:
                             type: string
                         attributes:
                             type: object
-                            required:
-                                - postId
-                                - userId
-                            properties:
-                                postId:
-                                    type: string
-                                userId:
-                                    type: string
                         relationships:
                             type: object
                             properties:
@@ -3178,13 +3170,6 @@ components:
                             type: string
                         type:
                             type: string
-                        attributes:
-                            type: object
-                            properties:
-                                postId:
-                                    type: string
-                                userId:
-                                    type: string
                         relationships:
                             type: object
                             properties:
diff --git a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml
index e3f2d6821..f69536b30 100644
--- a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml
+++ b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml
@@ -3155,16 +3155,6 @@ components:
                     properties:
                         type:
                             type: string
-                        attributes:
-                            type: object
-                            required:
-                                - postId
-                                - userId
-                            properties:
-                                postId:
-                                    type: string
-                                userId:
-                                    type: string
                         relationships:
                             type: object
                             properties:
@@ -3192,13 +3182,6 @@ components:
                             type: string
                         type:
                             type: string
-                        attributes:
-                            type: object
-                            properties:
-                                postId:
-                                    type: string
-                                userId:
-                                    type: string
                         relationships:
                             type: object
                             properties:
diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts
index 5021a9927..ca26ffabe 100644
--- a/packages/schema/src/plugins/zod/generator.ts
+++ b/packages/schema/src/plugins/zod/generator.ts
@@ -10,6 +10,7 @@ import {
     isEnumFieldReference,
     isForeignKeyField,
     isFromStdlib,
+    isIdField,
     parseOptionAsStrings,
     resolvePath,
 } from '@zenstackhq/sdk';
@@ -291,8 +292,10 @@ export class ZodSchemaGenerator {
         sf.replaceWithText((writer) => {
             const scalarFields = model.fields.filter(
                 (field) =>
+                    // id fields are always included
+                    isIdField(field) ||
                     // regular fields only
-                    !isDataModel(field.type.reference?.ref) && !isForeignKeyField(field)
+                    (!isDataModel(field.type.reference?.ref) && !isForeignKeyField(field))
             );
 
             const relations = model.fields.filter((field) => isDataModel(field.type.reference?.ref));
diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts
index 2e0bcaec5..e4ec06ff7 100644
--- a/packages/server/src/api/rest/index.ts
+++ b/packages/server/src/api/rest/index.ts
@@ -720,8 +720,9 @@ class RequestHandler extends APIHandlerBase {
         const attributes: any = parsed.data.attributes;
 
         if (attributes) {
-            const schemaName = `${upperCaseFirst(type)}${upperCaseFirst(mode)}Schema`;
-            // zod-parse attributes if a schema is provided
+            // use the zod schema (that only contains non-relation fields) to validate the payload,
+            // if available
+            const schemaName = `${upperCaseFirst(type)}${upperCaseFirst(mode)}ScalarSchema`;
             const payloadSchema = zodSchemas?.models?.[schemaName];
             if (payloadSchema) {
                 const parsed = payloadSchema.safeParse(attributes);
@@ -756,6 +757,7 @@ class RequestHandler extends APIHandlerBase {
         }
 
         const { error, attributes, relationships } = this.processRequestBody(type, requestBody, zodSchemas, 'create');
+
         if (error) {
             return error;
         }
@@ -776,18 +778,16 @@ class RequestHandler extends APIHandlerBase {
 
                 if (relationInfo.isCollection) {
                     createPayload.data[key] = {
-                        connect: enumerate(data.data).map((item: any) => ({
-                            [this.makePrismaIdKey(relationInfo.idFields)]: item.id,
-                        })),
+                        connect: enumerate(data.data).map((item: any) =>
+                            this.makeIdConnect(relationInfo.idFields, item.id)
+                        ),
                     };
                 } else {
                     if (typeof data.data !== 'object') {
                         return this.makeError('invalidRelationData');
                     }
                     createPayload.data[key] = {
-                        connect: {
-                            [this.makePrismaIdKey(relationInfo.idFields)]: data.data.id,
-                        },
+                        connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
                     };
                 }
 
@@ -868,9 +868,7 @@ class RequestHandler extends APIHandlerBase {
             } else {
                 updateArgs.data = {
                     [relationship]: {
-                        connect: {
-                            [this.makePrismaIdKey(relationInfo.idFields)]: parsed.data.data.id,
-                        },
+                        connect: this.makeIdConnect(relationInfo.idFields, parsed.data.data.id),
                     },
                 };
             }
@@ -1261,6 +1259,22 @@ class RequestHandler extends APIHandlerBase {
         return idFields.reduce((acc, curr) => ({ ...acc, [curr.name]: true }), {});
     }
 
+    private makeIdConnect(idFields: FieldInfo[], id: string | number) {
+        if (idFields.length === 1) {
+            return { [idFields[0].name]: this.coerce(idFields[0].type, id) };
+        } else {
+            return {
+                [this.makePrismaIdKey(idFields)]: idFields.reduce(
+                    (acc, curr, idx) => ({
+                        ...acc,
+                        [curr.name]: this.coerce(curr.type, `${id}`.split(this.idDivider)[idx]),
+                    }),
+                    {}
+                ),
+            };
+        }
+    }
+
     private makeIdKey(idFields: FieldInfo[]) {
         return idFields.map((idf) => idf.name).join(this.idDivider);
     }
diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts
index 3fee62d9a..1b5463650 100644
--- a/packages/server/tests/api/rest.test.ts
+++ b/packages/server/tests/api/rest.test.ts
@@ -74,8 +74,17 @@ describe('REST server tests', () => {
         superLike Boolean
         post Post @relation(fields: [postId], references: [id])
         user User @relation(fields: [userId], references: [myId])
+        likeInfos PostLikeInfo[]
         @@id([postId, userId])
     }
+
+    model PostLikeInfo {
+        id Int @id @default(autoincrement())
+        text String
+        postId Int
+        userId String
+        postLike PostLike @relation(fields: [postId, userId], references: [postId, userId])
+    }
     `;
 
         beforeAll(async () => {
@@ -1765,6 +1774,32 @@ describe('REST server tests', () => {
 
                     expect(r.status).toBe(201);
                 });
+
+                it('create an entity related to an entity with compound id', async () => {
+                    await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } });
+                    await prisma.post.create({ data: { id: 1, title: 'Post1' } });
+                    await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } });
+
+                    const r = await handler({
+                        method: 'post',
+                        path: '/postLikeInfo',
+                        query: {},
+                        requestBody: {
+                            data: {
+                                type: 'postLikeInfo',
+                                attributes: { text: 'LikeInfo1' },
+                                relationships: {
+                                    postLike: {
+                                        data: { type: 'postLike', id: `1${idDivider}user1` },
+                                    },
+                                },
+                            },
+                        },
+                        prisma,
+                    });
+
+                    expect(r.status).toBe(201);
+                });
             });
 
             describe('PUT', () => {

From 35ea74fa727b7dd88edc3a6ddb3db25446967b7d Mon Sep 17 00:00:00 2001
From: Thomas Sunde Nielsen <me@thomassnielsen.com>
Date: Fri, 25 Oct 2024 18:06:25 +0200
Subject: [PATCH 05/20] Fix for filtering by compound id (#1806)

---
 packages/server/src/api/rest/index.ts | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts
index e4ec06ff7..1c9c56f4e 100644
--- a/packages/server/src/api/rest/index.ts
+++ b/packages/server/src/api/rest/index.ts
@@ -1234,11 +1234,11 @@ class RequestHandler extends APIHandlerBase {
         return r.toString();
     }
 
-    private makePrismaIdFilter(idFields: FieldInfo[], resourceId: string) {
+    private makePrismaIdFilter(idFields: FieldInfo[], resourceId: string, nested: boolean = true) {
         const decodedId = decodeURIComponent(resourceId);
         if (idFields.length === 1) {
             return { [idFields[0].name]: this.coerce(idFields[0].type, decodedId) };
-        } else {
+        } else if (nested) {
             return {
                 // TODO: support `@@id` with custom name
                 [idFields.map((idf) => idf.name).join(prismaIdDivider)]: idFields.reduce(
@@ -1249,6 +1249,14 @@ class RequestHandler extends APIHandlerBase {
                     {}
                 ),
             };
+        } else {
+            return idFields.reduce(
+                (acc, curr, idx) => ({
+                    ...acc,
+                    [curr.name]: this.coerce(curr.type, decodedId.split(this.idDivider)[idx]),
+                }),
+                {}
+            );
         }
     }
 
@@ -1608,11 +1616,11 @@ class RequestHandler extends APIHandlerBase {
                 const values = value.split(',').filter((i) => i);
                 const filterValue =
                     values.length > 1
-                        ? { OR: values.map((v) => this.makePrismaIdFilter(info.idFields, v)) }
-                        : this.makePrismaIdFilter(info.idFields, value);
+                        ? { OR: values.map((v) => this.makePrismaIdFilter(info.idFields, v, false)) }
+                        : this.makePrismaIdFilter(info.idFields, value, false);
                 return { some: filterValue };
             } else {
-                return { is: this.makePrismaIdFilter(info.idFields, value) };
+                return { is: this.makePrismaIdFilter(info.idFields, value, false) };
             }
         } else {
             const coerced = this.coerce(fieldInfo.type, value);

From 28b130ffa31a070e52a33eba413b78518a21d237 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Fri, 25 Oct 2024 13:47:26 -0700
Subject: [PATCH 06/20] fix(server): change nextjs adapter's context params to
 be Promise to satisfy Next15's linter (#1809)

---
 packages/server/src/next/app-route-handler.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/server/src/next/app-route-handler.ts b/packages/server/src/next/app-route-handler.ts
index 8347c5eb0..d0894a816 100644
--- a/packages/server/src/next/app-route-handler.ts
+++ b/packages/server/src/next/app-route-handler.ts
@@ -6,10 +6,10 @@ import { AppRouteRequestHandlerOptions } from '.';
 import { RPCApiHandler } from '../api';
 import { loadAssets } from '../shared';
 
-type Context = { params: Promise<{ path: string[] }> | { path: string[] } };
+type Context = { params: Promise<{ path: string[] }> };
 
 /**
- * Creates a Next.js 13 "app dir" API route request handler which encapsulates Prisma CRUD operations.
+ * Creates a Next.js "app dir" API route request handler which encapsulates Prisma CRUD operations.
  *
  * @remarks Since Next.js 15, `context.params` is asynchronous and must be awaited.
  * @param options Options for initialization
@@ -28,7 +28,7 @@ export default function factory(
             return NextResponse.json({ message: 'unable to get prisma from request context' }, { status: 500 });
         }
 
-        let params: Context['params'];
+        let params: Awaited<Context['params']>;
         const url = new URL(req.url);
         const query = Object.fromEntries(url.searchParams);
 

From 77817f52ae84640413ae81bb668add0af7a76dd2 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Tue, 29 Oct 2024 12:33:23 -0700
Subject: [PATCH 07/20] chore: bump version (#1814)

---
 package.json                                 | 2 +-
 packages/ide/jetbrains/build.gradle.kts      | 2 +-
 packages/ide/jetbrains/package.json          | 2 +-
 packages/language/package.json               | 2 +-
 packages/misc/redwood/package.json           | 2 +-
 packages/plugins/openapi/package.json        | 2 +-
 packages/plugins/swr/package.json            | 2 +-
 packages/plugins/tanstack-query/package.json | 2 +-
 packages/plugins/trpc/package.json           | 2 +-
 packages/runtime/package.json                | 2 +-
 packages/schema/package.json                 | 2 +-
 packages/sdk/package.json                    | 2 +-
 packages/server/package.json                 | 2 +-
 packages/testtools/package.json              | 2 +-
 14 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/package.json b/package.json
index 94237146e..519137c35 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "zenstack-monorepo",
-    "version": "2.7.4",
+    "version": "2.8.0",
     "description": "",
     "scripts": {
         "build": "pnpm -r build",
diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts
index a2e6892fe..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.4"
+version = "2.8.0"
 
 repositories {
     mavenCentral()
diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json
index 5a43c79e0..372462b17 100644
--- a/packages/ide/jetbrains/package.json
+++ b/packages/ide/jetbrains/package.json
@@ -1,6 +1,6 @@
 {
   "name": "jetbrains",
-  "version": "2.7.4",
+  "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 f5b30d6e0..cf01ce9ad 100644
--- a/packages/language/package.json
+++ b/packages/language/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@zenstackhq/language",
-    "version": "2.7.4",
+    "version": "2.8.0",
     "displayName": "ZenStack modeling language compiler",
     "description": "ZenStack modeling language compiler",
     "homepage": "https://zenstack.dev",
diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json
index 752d79017..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.4",
+    "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 ed83d25b2..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.4",
+  "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 42de62697..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.4",
+    "version": "2.8.0",
     "description": "ZenStack plugin for generating SWR hooks",
     "main": "index.js",
     "repository": {
diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json
index 05a97815c..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.4",
+    "version": "2.8.0",
     "description": "ZenStack plugin for generating tanstack-query hooks",
     "main": "index.js",
     "exports": {
diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json
index 0e7f4afe5..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.4",
+    "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 088bde289..168a07850 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.4",
+    "version": "2.8.0",
     "description": "Runtime of ZenStack for both client-side and server-side environments.",
     "repository": {
         "type": "git",
diff --git a/packages/schema/package.json b/packages/schema/package.json
index efb704ba1..9a4234549 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.4",
+    "version": "2.8.0",
     "author": {
         "name": "ZenStack Team"
     },
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index ea62b7d44..8922f6ce3 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@zenstackhq/sdk",
-    "version": "2.7.4",
+    "version": "2.8.0",
     "description": "ZenStack plugin development SDK",
     "main": "index.js",
     "scripts": {
diff --git a/packages/server/package.json b/packages/server/package.json
index 49d897cef..064fd1ea1 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@zenstackhq/server",
-    "version": "2.7.4",
+    "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 ad909bdb7..ec5208c81 100644
--- a/packages/testtools/package.json
+++ b/packages/testtools/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@zenstackhq/testtools",
-  "version": "2.7.4",
+  "version": "2.8.0",
   "description": "ZenStack Test Tools",
   "main": "index.js",
   "private": true,

From d223819d29be241e37e26deafdf857753eb6f109 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Tue, 29 Oct 2024 17:08:27 -0700
Subject: [PATCH 08/20] fix(zmodel): prefer to use triple-slash comments as
 ZModel documentation (#1817)

---
 .../zmodel-documentation-provider.ts          | 16 +++++++
 .../src/language-server/zmodel-module.ts      |  6 ++-
 .../zmodel-workspace-manager.ts               |  2 +-
 .../src/plugins/prisma/schema-generator.ts    | 43 +++++++++++--------
 packages/schema/src/res/starter.zmodel        | 12 ++----
 .../tests/generator/prisma-generator.test.ts  | 41 +++++++++++++++++-
 6 files changed, 90 insertions(+), 30 deletions(-)
 create mode 100644 packages/schema/src/language-server/zmodel-documentation-provider.ts

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<ZModelServices, PartialLangiumServices & ZMode
     parser: {
         GrammarConfig: (services) => 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<string>();
 
     protected async loadAdditionalDocuments(
diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts
index 54d111d88..28a886e47 100644
--- a/packages/schema/src/plugins/prisma/schema-generator.ts
+++ b/packages/schema/src/plugins/prisma/schema-generator.ts
@@ -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<string, string>();
@@ -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,12 +291,9 @@ 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
         this.generateDelegateRelationForBase(model, decl);
@@ -763,11 +769,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 +781,6 @@ export class PrismaSchemaGenerator {
             }
         }
 
-        // user defined comments pass-through
-        field.comments.forEach((c) => result.addComment(c));
         return result;
     }
 
@@ -898,12 +900,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 +910,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/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/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');

From daa38398d73d2dccd965a25eb29424276cada03f Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Tue, 29 Oct 2024 21:24:49 -0700
Subject: [PATCH 09/20] fix(delegate): delegate model shouldn't inherit
 `@@index` from an indirect abstract base (#1818)

---
 packages/schema/src/utils/ast-utils.ts    | 27 +++++++++++--
 packages/sdk/src/utils.ts                 | 21 ++++++++++
 tests/regression/tests/issue-1786.test.ts | 48 +++++++++++++++++++++++
 3 files changed, 92 insertions(+), 4 deletions(-)
 create mode 100644 tests/regression/tests/issue-1786.test.ts

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<T extends InheritableNode>(
     node: T,
diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts
index d6c4c0fd5..6fbcfcbf3 100644
--- a/packages/sdk/src/utils.ts
+++ b/packages/sdk/src/utils.ts
@@ -569,3 +569,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/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
+    }           
+        `
+        );
+    });
+});

From be28f2e731b4edf2409d17318c809978a474b0ad Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Wed, 30 Oct 2024 15:50:45 -0700
Subject: [PATCH 10/20] fix: support `check()` policy function call in
 permission checkers (#1820)

---
 .../enhancements/node/policy/policy-utils.ts  |  53 +++++++-
 .../runtime/src/enhancements/node/types.ts    |  19 ++-
 .../enhancer/policy/constraint-transformer.ts |  32 +++++
 .../enhancements/with-policy/checker.test.ts  | 113 +++++++++++++++++-
 4 files changed, 213 insertions(+), 4 deletions(-)

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/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<string>(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/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();
+    });
 });

From 2bb897043400cd653fffcac0eb2e41b412e43da3 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Thu, 31 Oct 2024 01:08:06 -0700
Subject: [PATCH 11/20] fix(delegate): self relation support (#1821)

---
 .../src/plugins/prisma/schema-generator.ts    | 152 +++++++++++-------
 .../with-delegate/enhanced-client.test.ts     |  97 +++++++++++
 2 files changed, 193 insertions(+), 56 deletions(-)

diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts
index 28a886e47..f3dcba460 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,
@@ -295,17 +294,17 @@ export class PrismaSchemaGenerator {
         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) {
@@ -403,7 +402,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) => {
@@ -418,10 +417,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);
@@ -450,10 +448,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),
                         ]);
@@ -467,12 +462,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
@@ -486,8 +481,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]
@@ -499,26 +494,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;
     }
@@ -596,13 +585,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
@@ -636,30 +623,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))
+                                )
+                            )
+                        ),
                     ])
                 );
             }
@@ -690,9 +715,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) {
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 }] });
+    });
 });

From 00ecb2a7486b7191261b8f7a0b497afef5aa25a1 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Thu, 31 Oct 2024 16:19:54 -0700
Subject: [PATCH 12/20] fix(hooks): add null check to data before further
 deserialization (#1822)

---
 packages/plugins/swr/src/runtime/index.ts             | 2 +-
 packages/plugins/tanstack-query/src/runtime/common.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/plugins/swr/src/runtime/index.ts b/packages/plugins/swr/src/runtime/index.ts
index 0ca4212cc..0d25c6709 100644
--- a/packages/plugins/swr/src/runtime/index.ts
+++ b/packages/plugins/swr/src/runtime/index.ts
@@ -420,7 +420,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/src/runtime/common.ts b/packages/plugins/tanstack-query/src/runtime/common.ts
index 2d6793c8a..946963888 100644
--- a/packages/plugins/tanstack-query/src/runtime/common.ts
+++ b/packages/plugins/tanstack-query/src/runtime/common.ts
@@ -213,7 +213,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 {

From d985a733f3ac8dea755c139c4f404f9be98f407a Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Thu, 31 Oct 2024 18:30:10 -0700
Subject: [PATCH 13/20] feat(repl): add CLI option to specify custom zenstack
 load path (#1823)

---
 packages/schema/src/cli/actions/repl.ts | 27 +++++++++++++++++++------
 packages/schema/src/cli/index.ts        |  3 ++-
 2 files changed, 23 insertions(+), 7 deletions(-)

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 <module>', 'path to Prisma client module')
+        .option('--load-path <path>', 'path to load modules generated by ZenStack')
+        .option('--prisma-client <path>', 'path to Prisma client module')
         .option('--debug', 'enable debug output')
         .option('--table', 'enable table format output')
         .action(replAction);

From f16225cf1dc0cf4c78665575788ddb485a4e01bf Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Thu, 31 Oct 2024 20:59:24 -0700
Subject: [PATCH 14/20] fix(delegate): orderBy with base field doesn't work
 when the clause is an array (#1824)

---
 .../runtime/src/enhancements/node/delegate.ts |  8 ++-
 tests/regression/tests/issue-1755.test.ts     | 61 +++++++++++++++++++
 2 files changed, 66 insertions(+), 3 deletions(-)
 create mode 100644 tests/regression/tests/issue-1755.test.ts

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/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' }],
+        });
+    });
+});

From 00ccb7395a0c27b5fef28157b48c56351eaa9a66 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Sun, 3 Nov 2024 19:24:57 -0800
Subject: [PATCH 15/20] feat(zmodel): "type" construct and strongly-typed Json
 fields (#1813)

---
 packages/language/src/generated/ast.ts        |  92 +++-
 packages/language/src/generated/grammar.ts    | 393 ++++++++++++++----
 packages/language/src/zmodel.langium          |  36 +-
 packages/language/syntaxes/zmodel.tmLanguage  |   2 +-
 .../language/syntaxes/zmodel.tmLanguage.json  |   2 +-
 packages/plugins/swr/src/generator.ts         |   5 +-
 .../plugins/tanstack-query/src/generator.ts   |   5 +-
 packages/runtime/src/cross/model-meta.ts      |  39 +-
 packages/runtime/src/cross/utils.ts           |  27 +-
 .../src/enhancements/edge/json-processor.ts   |   1 +
 .../enhancements/node/create-enhancement.ts   |  13 +
 .../src/enhancements/node/default-auth.ts     |  46 +-
 .../src/enhancements/node/json-processor.ts   |  92 ++++
 packages/schema/src/cli/plugin-runner.ts      |   8 +-
 .../attribute-application-validator.ts        |  10 +
 .../validator/datamodel-validator.ts          |  20 +-
 .../validator/typedef-validator.ts            |  23 +
 .../validator/zmodel-validator.ts             |   7 +
 .../src/plugins/enhancer/enhance/index.ts     | 155 +++++--
 .../enhance/model-typedef-generator.ts        |  63 +++
 .../src/plugins/enhancer/model-meta/index.ts  |   5 +-
 .../enhancer/policy/policy-guard-generator.ts |  14 +-
 .../src/plugins/prisma/schema-generator.ts    |  28 +-
 packages/schema/src/plugins/zod/generator.ts  | 129 ++++--
 .../schema/src/plugins/zod/transformer.ts     | 157 ++++---
 .../src/plugins/zod/utils/schema-gen.ts       |  31 +-
 packages/schema/src/res/stdlib.zmodel         |  43 +-
 .../validation/attribute-validation.test.ts   |  15 +
 packages/sdk/src/model-meta-generator.ts      | 114 +++--
 packages/sdk/src/utils.ts                     |   6 +-
 packages/sdk/src/validation.ts                |  35 +-
 .../tests/enhancements/json/crud.test.ts      | 270 ++++++++++++
 .../tests/enhancements/json/typing.test.ts    | 181 ++++++++
 .../enhancements/json/validation.test.ts      |  39 ++
 tests/integration/tests/plugins/zod.test.ts   |  56 +++
 35 files changed, 1822 insertions(+), 340 deletions(-)
 create mode 120000 packages/runtime/src/enhancements/edge/json-processor.ts
 create mode 100644 packages/runtime/src/enhancements/node/json-processor.ts
 create mode 100644 packages/schema/src/language-server/validator/typedef-validator.ts
 create mode 100644 packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts
 create mode 100644 tests/integration/tests/enhancements/json/crud.test.ts
 create mode 100644 tests/integration/tests/enhancements/json/typing.test.ts
 create mode 100644 tests/integration/tests/enhancements/json/validation.test.ts

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<AttributeArg>
     decl: Reference<Attribute>
@@ -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<string>
+    fields: Array<TypeDefField>
+    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<DataModelFieldAttribute>
+    comments: Array<string>
+    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<TypeDef>
+    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 @@
         <key>name</key>
         <string>keyword.control.zmodel</string>
         <key>match</key>
-        <string>\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</string>
+        <string>\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</string>
       </dict>
       <dict>
         <key>name</key>
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/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/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<string>(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/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<string, FieldInfo>;
+};
+
 /**
  * ZModel data model metadata
  */
@@ -152,6 +172,11 @@ export type ModelMeta = {
      */
     models: Record<string, ModelInfo>;
 
+    /**
+     * Type defs
+     */
+    typeDefs?: Record<string, TypeDefInfo>;
+
     /**
      * 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<T1, T2>(x: Enumerable<T1>, y: Enumerable<T2>): 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<Throw extends boolean = false>(
     modelMeta: ModelMeta,
     model: string,
@@ -72,6 +78,25 @@ export function getModelInfo<Throw extends boolean = false>(
     return info;
 }
 
+/**
+ * Gets info for a type def.
+ */
+export function getTypeDefInfo<Throw extends boolean = false>(
+    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<DbClient extends object>(
 
     // 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<DbClient extends object>(
         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/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<DbClient extends object = any>(
+    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<T>(_method: PrismaProxyActions, data: T): Promise<T> {
+        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/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<Attri
             accept('error', `attribute "${decl.name}" cannot be used on this type of field`, { node: attr });
         }
 
+        if (isTypeDefField(targetDecl) && !hasAttribute(decl, '@@@supportTypeDef')) {
+            accept('error', `attribute "${decl.name}" cannot be used on type declaration fields`, { node: attr });
+        }
+
         const filledParams = new Set<AttributeParam>();
 
         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<DataModel> {
         }
 
         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<TypeDef> {
+    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/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts
index 34cf26640..3230d72f9 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<DbClient extends object>(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,27 @@ 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();
+
+        // if (delegateInfo.length > 0) {
+        //     // transform types for delegated models
+        //     this.transformDelegate(sf, sfNew, delegateInfo);
+        //     sfNew.formatText();
+        // } else {
+
+        //     this.transformJsonFields(sf, sfNew);
+
+        //     // // just copy
+        //     // sfNew.replaceWithText(sf.getFullText());
+        // }
+
         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 +504,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 `$<DelegateModel>Payload` type of delegate model to a union of concrete types
         const typeName = typeAlias.getName();
@@ -677,4 +750,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/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 f3dcba460..9a787ab8b 100644
--- a/packages/schema/src/plugins/prisma/schema-generator.ts
+++ b/packages/schema/src/plugins/prisma/schema-generator.ts
@@ -23,6 +23,7 @@ import {
     isNullExpr,
     isReferenceExpr,
     isStringLiteral,
+    isTypeDef,
     LiteralExpr,
     Model,
     NumberLiteral,
@@ -785,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))
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<string>();
+        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<string>();
+        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<string>();
-            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<string>();
-            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<string[]>((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<Buffer | Uint8Array>(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<string[]>((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<Buffer | Uint8Array>(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<string>(pathArg)?.join(`', '`)}'],` : '';
+            const path =
+                pathArg && isArrayExpr(pathArg) ? `path: ['${getLiteralArray<string>(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/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/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/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<string, string>;
 };
 
-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 6fbcfcbf3..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) {
diff --git a/packages/sdk/src/validation.ts b/packages/sdk/src/validation.ts
index e7edc21fc..8872b667e 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<DataModel | TypeDef> = 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);
+            } else {
+                return field.attributes.some((attr) => isValidationAttribute(attr));
+            }
+        })
+    ) {
         return true;
     }
 
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..1905a179e
--- /dev/null
+++ b/tests/integration/tests/enhancements/json/typing.test.ts
@@ -0,0 +1,181 @@
+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 } } });
+}
+                `,
+                    },
+                ],
+            }
+        );
+    });
+});
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/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,
+        });
+    });
 });

From ef6d529992e05cd91dc0bff4e798e448def970c4 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Mon, 4 Nov 2024 14:44:59 -0800
Subject: [PATCH 16/20] chore: README and JetBrains changelog update (#1828)

---
 README.md                           | 26 +++++++++++++-------------
 packages/ide/jetbrains/CHANGELOG.md |  6 ++++++
 2 files changed, 19 insertions(+), 13 deletions(-)

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!
    <td align="center"><a href="https://www.mermaidchart.com/"><img src="https://avatars.githubusercontent.com/u/117662492?s=200&v=4" width="100" style="border-radius:50%" alt="Mermaid Chart"/><br />Mermaid Chart</a></td>
    <td align="center"><a href="https://coderabbit.ai/"><img src="https://avatars.githubusercontent.com/u/132028505?v=4" width="100" style="border-radius:50%" alt="CodeRabbit"/><br />CodeRabbit</a></td>
    <td align="center"><a href="https://github.com/j0hannr"><img src="https://avatars.githubusercontent.com/u/52762073?v=4" width="100" style="border-radius:50%" alt="Johann Rohn"/><br />Johann Rohn</a></td>
-   <td align="center"><a href="https://github.com/baenie"><img src="https://avatars.githubusercontent.com/u/58309104?v=4" width="100" style="border-radius:50%" alt="Benjamin Zecirovic"/><br />Benjamin Zecirovic</a></td>
   </tr>
 </table>
 
@@ -249,6 +248,7 @@ Thank you for your generous support!
 
 <table>
   <tr>
+   <td align="center"><a href="https://github.com/baenie"><img src="https://avatars.githubusercontent.com/u/58309104?v=4" width="100" style="border-radius:50%" alt="Benjamin Zecirovic"/><br />Benjamin Zecirovic</a></td>
    <td align="center"><a href="https://github.com/umussetu"><img src="https://avatars.githubusercontent.com/u/152648499?v=4" width="100" style="border-radius:50%" alt="Ulric"/><br />Ulric</a></td>
    <td align="center"><a href="https://github.com/iamfj"><img src="https://avatars.githubusercontent.com/u/24557998?v=4" width="100" style="border-radius:50%" alt="Fabian Jocks"/><br />Fabian Jocks</a></td>
   </tr>
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.

From e19a3777d8c368e41d334ffd87f2feac2fd6fa47 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Tue, 5 Nov 2024 10:29:17 -0800
Subject: [PATCH 17/20] chore: Prisma 5.22 support (#1829)

---
 packages/runtime/package.json                 |   2 +-
 packages/schema/package.json                  |   4 +-
 packages/sdk/package.json                     |   4 +-
 pnpm-lock.yaml                                | 124 +++++++++---------
 script/test-scaffold.ts                       |   2 +-
 tests/integration/test-run/package.json       |   4 +-
 tests/integration/tests/cli/plugins.test.ts   |   4 +-
 .../nextjs/test-project/package.json          |   4 +-
 .../frameworks/trpc/test-project/package.json |   4 +-
 9 files changed, 76 insertions(+), 76 deletions(-)

diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 168a07850..f63f40ced 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -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/schema/package.json b/packages/schema/package.json
index 9a4234549..a99ebeee7 100644
--- a/packages/schema/package.json
+++ b/packages/schema/package.json
@@ -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/sdk/package.json b/packages/sdk/package.json
index 8922f6ce3..6222c5500 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -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/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/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"
   }
 }

From 5f15fe0171d9f7d34a4618c576a834272e6e2b2e Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Tue, 5 Nov 2024 18:50:43 -0800
Subject: [PATCH 18/20] chore: remove commented code (#1830)

---
 .../schema/src/plugins/enhancer/enhance/index.ts   | 14 +-------------
 1 file changed, 1 insertion(+), 13 deletions(-)

diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts
index 3230d72f9..0b69b6a25 100644
--- a/packages/schema/src/plugins/enhancer/enhance/index.ts
+++ b/packages/schema/src/plugins/enhancer/enhance/index.ts
@@ -354,20 +354,8 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara
         this.transformPrismaTypes(sf, sfNew, delegateInfo);
 
         this.generateExtraTypes(sfNew);
-        sfNew.formatText();
-
-        // if (delegateInfo.length > 0) {
-        //     // transform types for delegated models
-        //     this.transformDelegate(sf, sfNew, delegateInfo);
-        //     sfNew.formatText();
-        // } else {
-
-        //     this.transformJsonFields(sf, sfNew);
-
-        //     // // just copy
-        //     // sfNew.replaceWithText(sf.getFullText());
-        // }
 
+        sfNew.formatText();
         await sfNew.save();
     }
 

From 9cc01d59f5cf2585768344060c8069667dc0f8c0 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Tue, 5 Nov 2024 19:18:34 -0800
Subject: [PATCH 19/20] chore: type coverage test (#1831)

---
 .../tests/enhancements/json/typing.test.ts    | 52 +++++++++++++++++++
 1 file changed, 52 insertions(+)

diff --git a/tests/integration/tests/enhancements/json/typing.test.ts b/tests/integration/tests/enhancements/json/typing.test.ts
index 1905a179e..9681bf015 100644
--- a/tests/integration/tests/enhancements/json/typing.test.ts
+++ b/tests/integration/tests/enhancements/json/typing.test.ts
@@ -171,6 +171,58 @@ async function main() {
     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 },
+    }
 }
                 `,
                     },

From 13f95d270adc92fd6a5658d7e4abc05edce8d1e6 Mon Sep 17 00:00:00 2001
From: Yiming <yiming@whimslab.io>
Date: Tue, 5 Nov 2024 19:33:57 -0800
Subject: [PATCH 20/20] fix: missing arg passing to recursion (#1832)

---
 packages/sdk/src/validation.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/sdk/src/validation.ts b/packages/sdk/src/validation.ts
index 8872b667e..9dbcd8f5c 100644
--- a/packages/sdk/src/validation.ts
+++ b/packages/sdk/src/validation.ts
@@ -33,7 +33,7 @@ export function hasValidationAttributes(
     if (
         decl.fields.some((field) => {
             if (isTypeDef(field.type.reference?.ref)) {
-                return hasValidationAttributes(field.type.reference?.ref);
+                return hasValidationAttributes(field.type.reference?.ref, seen);
             } else {
                 return field.attributes.some((attr) => isValidationAttribute(attr));
             }