From 2367fc7e1f165f87dd1cb75b9eb905fe35c79101 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko's AI Agent Date: Tue, 2 Jun 2026 15:18:17 +0200 Subject: [PATCH 1/7] =?UTF-8?q?feat(sqlite-demo):=20add=20Post=E2=86=94Tag?= =?UTF-8?q?=20M:N=20relation=20+=20emit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Tag model, PostTag junction (composite PK post_tag_pkey), and rel.manyToMany() declarations on both Post.tags and Tag.posts. Re-emits contract.json / contract.d.ts; cardinality 'N:M' + through descriptor present in the generated artifacts. Signed-off-by: Alexey Orlenko's AI Agent --- .../prisma/contract.ts | 37 ++ .../src/prisma/contract.d.ts | 411 ++++++++++++++--- .../src/prisma/contract.json | 424 ++++++++++++++---- 3 files changed, 705 insertions(+), 167 deletions(-) diff --git a/examples/prisma-next-demo-sqlite/prisma/contract.ts b/examples/prisma-next-demo-sqlite/prisma/contract.ts index cb0ae3d28a..aeebb5bb45 100644 --- a/examples/prisma-next-demo-sqlite/prisma/contract.ts +++ b/examples/prisma-next-demo-sqlite/prisma/contract.ts @@ -20,6 +20,22 @@ export const contract = defineContract({}, ({ field, model }) => { }, }); + const Tag = model('Tag', { + fields: { + id: field.id.uuidv4(), + label: field.column(textColumn), + }, + }); + + const PostTag = model('PostTag', { + fields: { + postId: field.uuid(), + tagId: field.uuid(), + }, + }).attributes(({ fields, constraints }) => ({ + id: constraints.id([fields.postId, fields.tagId], { name: 'post_tag_pkey' }), + })); + return { models: { User: User.relations({ @@ -29,6 +45,11 @@ export const contract = defineContract({}, ({ field, model }) => { }), Post: Post.relations({ user: rel.belongsTo(User, { from: 'userId', to: 'id' }), + tags: rel.manyToMany(() => Tag, { + through: () => PostTag, + from: 'postId', + to: 'tagId', + }), }).sql(({ cols, constraints }) => ({ table: 'post', foreignKeys: [ @@ -37,6 +58,22 @@ export const contract = defineContract({}, ({ field, model }) => { }), ], })), + Tag: Tag.relations({ + posts: rel.manyToMany(() => Post, { + through: () => PostTag, + from: 'tagId', + to: 'postId', + }), + }).sql({ + table: 'tag', + }), + PostTag: PostTag.sql(({ cols, constraints }) => ({ + table: 'post_tag', + foreignKeys: [ + constraints.foreignKey(cols.postId, Post.refs.id, { name: 'post_tag_postId_fkey' }), + constraints.foreignKey(cols.tagId, Tag.refs.id, { name: 'post_tag_tagId_fkey' }), + ], + })), }, }; }); diff --git a/examples/prisma-next-demo-sqlite/src/prisma/contract.d.ts b/examples/prisma-next-demo-sqlite/src/prisma/contract.d.ts index ada56e206c..ff5dd617eb 100644 --- a/examples/prisma-next-demo-sqlite/src/prisma/contract.d.ts +++ b/examples/prisma-next-demo-sqlite/src/prisma/contract.d.ts @@ -17,9 +17,9 @@ import type { } from '@prisma-next/contract/types'; export type StorageHash = - StorageHashBase<'sha256:5736eda44575a2e995e8c1d5ed6d7884859a882e9551844f776d2a497c6d0154'>; + StorageHashBase<'sha256:194cf0bce4a19c7191f9e4ae8670de21707c48678d7b8998d38708a506488c88'>; export type ExecutionHash = - ExecutionHashBase<'sha256:aa79980d83d295977c2ca3fd5a1407ea2bc67b4538b19971512d2f47ff71338e'>; + ExecutionHashBase<'sha256:70c6ceb0ed79d5888519ec84bf32e31a056283fa8d5c7b5c1ba65b709f8595c8'>; export type ProfileHash = ProfileHashBase<'sha256:3cc333ecad9f3f4c7229370a9d2c37e908cdce0f8d2e9fb132d50605b024eff2'>; @@ -37,6 +37,14 @@ export type FieldOutputTypes = { readonly userId: CodecTypes['sql/char@1']['output']; readonly createdAt: CodecTypes['sqlite/datetime@1']['output']; }; + readonly PostTag: { + readonly postId: CodecTypes['sql/char@1']['output']; + readonly tagId: CodecTypes['sql/char@1']['output']; + }; + readonly Tag: { + readonly id: CodecTypes['sql/char@1']['output']; + readonly label: CodecTypes['sqlite/text@1']['output']; + }; readonly User: { readonly id: CodecTypes['sql/char@1']['output']; readonly email: CodecTypes['sqlite/text@1']['output']; @@ -51,6 +59,14 @@ export type FieldInputTypes = { readonly userId: CodecTypes['sql/char@1']['input']; readonly createdAt: CodecTypes['sqlite/datetime@1']['input']; }; + readonly PostTag: { + readonly postId: CodecTypes['sql/char@1']['input']; + readonly tagId: CodecTypes['sql/char@1']['input']; + }; + readonly Tag: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly label: CodecTypes['sqlite/text@1']['input']; + }; readonly User: { readonly id: CodecTypes['sql/char@1']['input']; readonly email: CodecTypes['sqlite/text@1']['input']; @@ -71,86 +87,157 @@ type ContractBase = Omit< readonly namespaces: { readonly __unbound__: { readonly id: '__unbound__'; - readonly kind: 'sqlite-namespace'; - readonly entries: { - readonly table: { - readonly post: { - columns: { - readonly id: { - readonly nativeType: 'character'; - readonly codecId: 'sql/char@1'; - readonly nullable: false; - readonly typeParams: { readonly length: 36 }; - }; - readonly title: { - readonly nativeType: 'text'; - readonly codecId: 'sqlite/text@1'; - readonly nullable: false; - }; - readonly userId: { - readonly nativeType: 'character'; - readonly codecId: 'sql/char@1'; - readonly nullable: false; - readonly typeParams: { readonly length: 36 }; + readonly kind: 'sql-namespace'; + readonly tables: { + readonly post: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly title: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly userId: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly createdAt: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/datetime@1'; + readonly nullable: false; + readonly default: { readonly kind: 'function'; readonly expression: 'now()' }; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly [ + { + readonly source: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'post'; + readonly columns: readonly ['userId']; }; - readonly createdAt: { - readonly nativeType: 'text'; - readonly codecId: 'sqlite/datetime@1'; - readonly nullable: false; - readonly default: { readonly kind: 'function'; readonly expression: 'now()' }; + readonly target: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'user'; + readonly columns: readonly ['id']; }; + readonly name: 'post_userId_fkey'; + readonly constraint: true; + readonly index: true; + }, + ]; + }; + readonly post_tag: { + columns: { + readonly postId: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly tagId: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; }; - primaryKey: { readonly columns: readonly ['id'] }; - uniques: readonly []; - indexes: readonly []; - foreignKeys: readonly [ - { - readonly source: { - readonly namespaceId: '__unbound__' & NamespaceId; - readonly tableName: 'post'; - readonly columns: readonly ['userId']; - }; - readonly target: { - readonly namespaceId: '__unbound__' & NamespaceId; - readonly tableName: 'user'; - readonly columns: readonly ['id']; - }; - readonly name: 'post_userId_fkey'; - readonly constraint: true; - readonly index: true; - }, - ]; }; - readonly user: { - columns: { - readonly id: { - readonly nativeType: 'character'; - readonly codecId: 'sql/char@1'; - readonly nullable: false; - readonly typeParams: { readonly length: 36 }; + primaryKey: { + readonly columns: readonly ['postId', 'tagId']; + readonly name: 'post_tag_pkey'; + }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly [ + { + readonly source: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'post_tag'; + readonly columns: readonly ['postId']; }; - readonly email: { - readonly nativeType: 'text'; - readonly codecId: 'sqlite/text@1'; - readonly nullable: false; + readonly target: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'post'; + readonly columns: readonly ['id']; }; - readonly displayName: { - readonly nativeType: 'text'; - readonly codecId: 'sqlite/text@1'; - readonly nullable: false; + readonly name: 'post_tag_postId_fkey'; + readonly constraint: true; + readonly index: true; + }, + { + readonly source: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'post_tag'; + readonly columns: readonly ['tagId']; }; - readonly createdAt: { - readonly nativeType: 'text'; - readonly codecId: 'sqlite/datetime@1'; - readonly nullable: false; - readonly default: { readonly kind: 'function'; readonly expression: 'now()' }; + readonly target: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'tag'; + readonly columns: readonly ['id']; }; + readonly name: 'post_tag_tagId_fkey'; + readonly constraint: true; + readonly index: true; + }, + ]; + }; + readonly tag: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly label: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly displayName: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly createdAt: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/datetime@1'; + readonly nullable: false; + readonly default: { readonly kind: 'function'; readonly expression: 'now()' }; }; - primaryKey: { readonly columns: readonly ['id'] }; - uniques: readonly []; - indexes: readonly []; - foreignKeys: readonly []; }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; }; }; }; @@ -197,6 +284,20 @@ type ContractBase = Omit< readonly targetFields: readonly ['id']; }; }; + readonly tags: { + readonly to: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Tag' }; + readonly cardinality: 'N:M'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['postId']; + }; + readonly through: { + readonly table: 'post_tag'; + readonly parentColumns: readonly ['postId']; + readonly childColumns: readonly ['tagId']; + readonly targetColumns: readonly ['id']; + }; + }; }; readonly storage: { readonly table: 'post'; @@ -209,6 +310,76 @@ type ContractBase = Omit< }; }; }; + readonly PostTag: { + readonly fields: { + readonly postId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly tagId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'post_tag'; + readonly fields: { + readonly postId: { readonly column: 'postId' }; + readonly tagId: { readonly column: 'tagId' }; + }; + }; + }; + readonly Tag: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly label: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + }; + readonly relations: { + readonly posts: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Post'; + }; + readonly cardinality: 'N:M'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['tagId']; + }; + readonly through: { + readonly table: 'post_tag'; + readonly parentColumns: readonly ['tagId']; + readonly childColumns: readonly ['postId']; + readonly targetColumns: readonly ['id']; + }; + }; + }; + readonly storage: { + readonly table: 'tag'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly label: { readonly column: 'label' }; + }; + }; + }; readonly User: { readonly fields: { readonly id: { @@ -265,6 +436,11 @@ type ContractBase = Omit< readonly roots: { readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'User' }; readonly post: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Post' }; + readonly tag: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Tag' }; + readonly post_tag: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'PostTag'; + }; }; readonly domain: { readonly namespaces: { @@ -309,6 +485,23 @@ type ContractBase = Omit< readonly targetFields: readonly ['id']; }; }; + readonly tags: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Tag'; + }; + readonly cardinality: 'N:M'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['postId']; + }; + readonly through: { + readonly table: 'post_tag'; + readonly parentColumns: readonly ['postId']; + readonly childColumns: readonly ['tagId']; + readonly targetColumns: readonly ['id']; + }; + }; }; readonly storage: { readonly table: 'post'; @@ -321,6 +514,76 @@ type ContractBase = Omit< }; }; }; + readonly PostTag: { + readonly fields: { + readonly postId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly tagId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'post_tag'; + readonly fields: { + readonly postId: { readonly column: 'postId' }; + readonly tagId: { readonly column: 'tagId' }; + }; + }; + }; + readonly Tag: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly label: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + }; + readonly relations: { + readonly posts: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Post'; + }; + readonly cardinality: 'N:M'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['tagId']; + }; + readonly through: { + readonly table: 'post_tag'; + readonly parentColumns: readonly ['tagId']; + readonly childColumns: readonly ['postId']; + readonly targetColumns: readonly ['id']; + }; + }; + }; + readonly storage: { + readonly table: 'tag'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly label: { readonly column: 'label' }; + }; + }; + }; readonly User: { readonly fields: { readonly id: { @@ -392,6 +655,10 @@ type ContractBase = Omit< readonly ref: { readonly table: 'post'; readonly column: 'id' }; readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; }, + { + readonly ref: { readonly table: 'tag'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, { readonly ref: { readonly table: 'user'; readonly column: 'id' }; readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; diff --git a/examples/prisma-next-demo-sqlite/src/prisma/contract.json b/examples/prisma-next-demo-sqlite/src/prisma/contract.json index 4b259517e9..be7bd54af2 100644 --- a/examples/prisma-next-demo-sqlite/src/prisma/contract.json +++ b/examples/prisma-next-demo-sqlite/src/prisma/contract.json @@ -8,6 +8,14 @@ "model": "Post", "namespace": "__unbound__" }, + "post_tag": { + "model": "PostTag", + "namespace": "__unbound__" + }, + "tag": { + "model": "Tag", + "namespace": "__unbound__" + }, "user": { "model": "User", "namespace": "__unbound__" @@ -55,6 +63,33 @@ } }, "relations": { + "tags": { + "cardinality": "N:M", + "on": { + "localFields": [ + "id" + ], + "targetFields": [ + "postId" + ] + }, + "through": { + "childColumns": [ + "tagId" + ], + "parentColumns": [ + "postId" + ], + "table": "post_tag", + "targetColumns": [ + "id" + ] + }, + "to": { + "model": "Tag", + "namespace": "__unbound__" + } + }, "user": { "cardinality": "N:1", "on": { @@ -90,6 +125,103 @@ "table": "post" } }, + "PostTag": { + "fields": { + "postId": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "tagId": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "postId": { + "column": "postId" + }, + "tagId": { + "column": "tagId" + } + }, + "table": "post_tag" + } + }, + "Tag": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "label": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + } + }, + "relations": { + "posts": { + "cardinality": "N:M", + "on": { + "localFields": [ + "id" + ], + "targetFields": [ + "tagId" + ] + }, + "through": { + "childColumns": [ + "postId" + ], + "parentColumns": [ + "tagId" + ], + "table": "post_tag", + "targetColumns": [ + "id" + ] + }, + "to": { + "model": "Post", + "namespace": "__unbound__" + } + } + }, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "label": { + "column": "label" + } + }, + "table": "tag" + } + }, "User": { "fields": { "createdAt": { @@ -167,118 +299,210 @@ "storage": { "namespaces": { "__unbound__": { - "entries": { - "table": { - "post": { - "columns": { - "createdAt": { - "codecId": "sqlite/datetime@1", - "default": { - "expression": "now()", - "kind": "function" - }, - "nativeType": "text", - "nullable": false + "id": "__unbound__", + "tables": { + "post": { + "columns": { + "createdAt": { + "codecId": "sqlite/datetime@1", + "default": { + "expression": "now()", + "kind": "function" }, - "id": { - "codecId": "sql/char@1", - "nativeType": "character", - "nullable": false, - "typeParams": { - "length": 36 - } + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "title": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + }, + "userId": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [ + { + "constraint": true, + "index": true, + "name": "post_userId_fkey", + "source": { + "columns": [ + "userId" + ], + "namespaceId": "__unbound__", + "tableName": "post" }, - "title": { - "codecId": "sqlite/text@1", - "nativeType": "text", - "nullable": false + "target": { + "columns": [ + "id" + ], + "namespaceId": "__unbound__", + "tableName": "user" + } + } + ], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + }, + "post_tag": { + "columns": { + "postId": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "tagId": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [ + { + "constraint": true, + "index": true, + "name": "post_tag_postId_fkey", + "source": { + "columns": [ + "postId" + ], + "namespaceId": "__unbound__", + "tableName": "post_tag" }, - "userId": { - "codecId": "sql/char@1", - "nativeType": "character", - "nullable": false, - "typeParams": { - "length": 36 - } + "target": { + "columns": [ + "id" + ], + "namespaceId": "__unbound__", + "tableName": "post" } }, - "foreignKeys": [ - { - "constraint": true, - "index": true, - "name": "post_userId_fkey", - "source": { - "columns": [ - "userId" - ], - "namespaceId": "__unbound__", - "tableName": "post" - }, - "target": { - "columns": [ - "id" - ], - "namespaceId": "__unbound__", - "tableName": "user" - } + { + "constraint": true, + "index": true, + "name": "post_tag_tagId_fkey", + "source": { + "columns": [ + "tagId" + ], + "namespaceId": "__unbound__", + "tableName": "post_tag" + }, + "target": { + "columns": [ + "id" + ], + "namespaceId": "__unbound__", + "tableName": "tag" } + } + ], + "indexes": [], + "primaryKey": { + "columns": [ + "postId", + "tagId" ], - "indexes": [], - "primaryKey": { - "columns": [ - "id" - ] + "name": "post_tag_pkey" + }, + "uniques": [] + }, + "tag": { + "columns": { + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } }, - "uniques": [] + "label": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + } }, - "user": { - "columns": { - "createdAt": { - "codecId": "sqlite/datetime@1", - "default": { - "expression": "now()", - "kind": "function" - }, - "nativeType": "text", - "nullable": false - }, - "displayName": { - "codecId": "sqlite/text@1", - "nativeType": "text", - "nullable": false - }, - "email": { - "codecId": "sqlite/text@1", - "nativeType": "text", - "nullable": false + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + }, + "user": { + "columns": { + "createdAt": { + "codecId": "sqlite/datetime@1", + "default": { + "expression": "now()", + "kind": "function" }, - "id": { - "codecId": "sql/char@1", - "nativeType": "character", - "nullable": false, - "typeParams": { - "length": 36 - } - } + "nativeType": "text", + "nullable": false + }, + "displayName": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false }, - "foreignKeys": [], - "indexes": [], - "primaryKey": { - "columns": [ - "id" - ] + "email": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false }, - "uniques": [] - } + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] } - }, - "id": "__unbound__" + } } }, - "storageHash": "sha256:5736eda44575a2e995e8c1d5ed6d7884859a882e9551844f776d2a497c6d0154" + "storageHash": "sha256:194cf0bce4a19c7191f9e4ae8670de21707c48678d7b8998d38708a506488c88" }, "execution": { - "executionHash": "sha256:aa79980d83d295977c2ca3fd5a1407ea2bc67b4538b19971512d2f47ff71338e", + "executionHash": "sha256:70c6ceb0ed79d5888519ec84bf32e31a056283fa8d5c7b5c1ba65b709f8595c8", "mutations": { "defaults": [ { @@ -291,6 +515,16 @@ "table": "post" } }, + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "tag" + } + }, { "onCreate": { "id": "uuidv4", From ba4f340135643f448921413ea20e36dd7979ee75 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko's AI Agent Date: Tue, 2 Jun 2026 15:21:51 +0200 Subject: [PATCH 2/7] feat(sqlite-demo): add M:N ORM client examples + seed + CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds five ORM client modules exercising the Post↔Tag N:M relation: - get-post-tags: .include('tags', ...) — include read across junction - get-posts-by-tag-filter: .some()/.none()/.every() on N:M relation - connect-post-tags: update + t.connect([{id}]) callback mutator - disconnect-post-tags: update + t.disconnect([{id}]) callback mutator - create-post-with-tags: create + t.create([...]) nested mutation Seed extended with Tag rows + junction rows (typescript/orm/demo tags linked to First Post and Second Post). CLI commands registered in main.ts. Migration refs committed for offline db:init reproducibility. Signed-off-by: Alexey Orlenko's AI Agent --- .../migrations/app/refs/db.contract.d.ts | 673 ++++++++++++++++++ .../migrations/app/refs/db.contract.json | 341 +++++++++ .../migrations/app/refs/db.json | 4 + .../prisma-next-demo-sqlite/scripts/seed.ts | 60 +- examples/prisma-next-demo-sqlite/src/main.ts | 82 ++- .../src/orm-client/connect-post-tags.ts | 29 + .../src/orm-client/create-post-with-tags.ts | 37 + .../src/orm-client/disconnect-post-tags.ts | 28 + .../src/orm-client/get-post-tags.ts | 18 + .../src/orm-client/get-posts-by-tag-filter.ts | 28 + 10 files changed, 1294 insertions(+), 6 deletions(-) create mode 100644 examples/prisma-next-demo-sqlite/migrations/app/refs/db.contract.d.ts create mode 100644 examples/prisma-next-demo-sqlite/migrations/app/refs/db.contract.json create mode 100644 examples/prisma-next-demo-sqlite/migrations/app/refs/db.json create mode 100644 examples/prisma-next-demo-sqlite/src/orm-client/connect-post-tags.ts create mode 100644 examples/prisma-next-demo-sqlite/src/orm-client/create-post-with-tags.ts create mode 100644 examples/prisma-next-demo-sqlite/src/orm-client/disconnect-post-tags.ts create mode 100644 examples/prisma-next-demo-sqlite/src/orm-client/get-post-tags.ts create mode 100644 examples/prisma-next-demo-sqlite/src/orm-client/get-posts-by-tag-filter.ts diff --git a/examples/prisma-next-demo-sqlite/migrations/app/refs/db.contract.d.ts b/examples/prisma-next-demo-sqlite/migrations/app/refs/db.contract.d.ts new file mode 100644 index 0000000000..6e028057fb --- /dev/null +++ b/examples/prisma-next-demo-sqlite/migrations/app/refs/db.contract.d.ts @@ -0,0 +1,673 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { CodecTypes as SqliteTypes } from '@prisma-next/adapter-sqlite/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:194cf0bce4a19c7191f9e4ae8670de21707c48678d7b8998d38708a506488c88'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:70c6ceb0ed79d5888519ec84bf32e31a056283fa8d5c7b5c1ba65b709f8595c8'>; +export type ProfileHash = + ProfileHashBase<'sha256:3cc333ecad9f3f4c7229370a9d2c37e908cdce0f8d2e9fb132d50605b024eff2'>; + +export type CodecTypes = SqliteTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = Record; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly Post: { + readonly id: CodecTypes['sql/char@1']['output']; + readonly title: CodecTypes['sqlite/text@1']['output']; + readonly userId: CodecTypes['sql/char@1']['output']; + readonly createdAt: CodecTypes['sqlite/datetime@1']['output']; + }; + readonly PostTag: { + readonly postId: CodecTypes['sql/char@1']['output']; + readonly tagId: CodecTypes['sql/char@1']['output']; + }; + readonly Tag: { + readonly id: CodecTypes['sql/char@1']['output']; + readonly label: CodecTypes['sqlite/text@1']['output']; + }; + readonly User: { + readonly id: CodecTypes['sql/char@1']['output']; + readonly email: CodecTypes['sqlite/text@1']['output']; + readonly displayName: CodecTypes['sqlite/text@1']['output']; + readonly createdAt: CodecTypes['sqlite/datetime@1']['output']; + }; +}; +export type FieldInputTypes = { + readonly Post: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly title: CodecTypes['sqlite/text@1']['input']; + readonly userId: CodecTypes['sql/char@1']['input']; + readonly createdAt: CodecTypes['sqlite/datetime@1']['input']; + }; + readonly PostTag: { + readonly postId: CodecTypes['sql/char@1']['input']; + readonly tagId: CodecTypes['sql/char@1']['input']; + }; + readonly Tag: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly label: CodecTypes['sqlite/text@1']['input']; + }; + readonly User: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['sqlite/text@1']['input']; + readonly displayName: CodecTypes['sqlite/text@1']['input']; + readonly createdAt: CodecTypes['sqlite/datetime@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'sql-namespace'; + readonly tables: { + readonly post: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly title: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly userId: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly createdAt: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/datetime@1'; + readonly nullable: false; + readonly default: { readonly kind: 'function'; readonly expression: 'now()' }; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly [ + { + readonly source: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'post'; + readonly columns: readonly ['userId']; + }; + readonly target: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'user'; + readonly columns: readonly ['id']; + }; + readonly name: 'post_userId_fkey'; + readonly constraint: true; + readonly index: true; + }, + ]; + }; + readonly post_tag: { + columns: { + readonly postId: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly tagId: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + }; + primaryKey: { + readonly columns: readonly ['postId', 'tagId']; + readonly name: 'post_tag_pkey'; + }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly [ + { + readonly source: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'post_tag'; + readonly columns: readonly ['postId']; + }; + readonly target: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'post'; + readonly columns: readonly ['id']; + }; + readonly name: 'post_tag_postId_fkey'; + readonly constraint: true; + readonly index: true; + }, + { + readonly source: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'post_tag'; + readonly columns: readonly ['tagId']; + }; + readonly target: { + readonly namespaceId: '__unbound__' & NamespaceId; + readonly tableName: 'tag'; + readonly columns: readonly ['id']; + }; + readonly name: 'post_tag_tagId_fkey'; + readonly constraint: true; + readonly index: true; + }, + ]; + }; + readonly tag: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly label: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly displayName: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly createdAt: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/datetime@1'; + readonly nullable: false; + readonly default: { readonly kind: 'function'; readonly expression: 'now()' }; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly Post: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly title: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly userId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly createdAt: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/datetime@1' }; + }; + }; + readonly relations: { + readonly user: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'User'; + }; + readonly cardinality: 'N:1'; + readonly on: { + readonly localFields: readonly ['userId']; + readonly targetFields: readonly ['id']; + }; + }; + readonly tags: { + readonly to: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Tag' }; + readonly cardinality: 'N:M'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['postId']; + }; + readonly through: { + readonly table: 'post_tag'; + readonly parentColumns: readonly ['postId']; + readonly childColumns: readonly ['tagId']; + readonly targetColumns: readonly ['id']; + }; + }; + }; + readonly storage: { + readonly table: 'post'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly title: { readonly column: 'title' }; + readonly userId: { readonly column: 'userId' }; + readonly createdAt: { readonly column: 'createdAt' }; + }; + }; + }; + readonly PostTag: { + readonly fields: { + readonly postId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly tagId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'post_tag'; + readonly fields: { + readonly postId: { readonly column: 'postId' }; + readonly tagId: { readonly column: 'tagId' }; + }; + }; + }; + readonly Tag: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly label: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + }; + readonly relations: { + readonly posts: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Post'; + }; + readonly cardinality: 'N:M'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['tagId']; + }; + readonly through: { + readonly table: 'post_tag'; + readonly parentColumns: readonly ['tagId']; + readonly childColumns: readonly ['postId']; + readonly targetColumns: readonly ['id']; + }; + }; + }; + readonly storage: { + readonly table: 'tag'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly label: { readonly column: 'label' }; + }; + }; + }; + readonly User: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly displayName: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly createdAt: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/datetime@1' }; + }; + }; + readonly relations: { + readonly posts: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Post'; + }; + readonly cardinality: '1:N'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['userId']; + }; + }; + }; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly displayName: { readonly column: 'displayName' }; + readonly createdAt: { readonly column: 'createdAt' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'sqlite'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'User' }; + readonly post: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Post' }; + readonly tag: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Tag' }; + readonly post_tag: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'PostTag'; + }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly Post: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly title: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly userId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly createdAt: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/datetime@1' }; + }; + }; + readonly relations: { + readonly user: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'User'; + }; + readonly cardinality: 'N:1'; + readonly on: { + readonly localFields: readonly ['userId']; + readonly targetFields: readonly ['id']; + }; + }; + readonly tags: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Tag'; + }; + readonly cardinality: 'N:M'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['postId']; + }; + readonly through: { + readonly table: 'post_tag'; + readonly parentColumns: readonly ['postId']; + readonly childColumns: readonly ['tagId']; + readonly targetColumns: readonly ['id']; + }; + }; + }; + readonly storage: { + readonly table: 'post'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly title: { readonly column: 'title' }; + readonly userId: { readonly column: 'userId' }; + readonly createdAt: { readonly column: 'createdAt' }; + }; + }; + }; + readonly PostTag: { + readonly fields: { + readonly postId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly tagId: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'post_tag'; + readonly fields: { + readonly postId: { readonly column: 'postId' }; + readonly tagId: { readonly column: 'tagId' }; + }; + }; + }; + readonly Tag: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly label: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + }; + readonly relations: { + readonly posts: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Post'; + }; + readonly cardinality: 'N:M'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['tagId']; + }; + readonly through: { + readonly table: 'post_tag'; + readonly parentColumns: readonly ['tagId']; + readonly childColumns: readonly ['postId']; + readonly targetColumns: readonly ['id']; + }; + }; + }; + readonly storage: { + readonly table: 'tag'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly label: { readonly column: 'label' }; + }; + }; + }; + readonly User: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly displayName: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly createdAt: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/datetime@1' }; + }; + }; + readonly relations: { + readonly posts: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Post'; + }; + readonly cardinality: '1:N'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['userId']; + }; + }; + }; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly displayName: { readonly column: 'displayName' }; + readonly createdAt: { readonly column: 'createdAt' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly sql: { + readonly enums: false; + readonly foreignKeys: true; + readonly jsonAgg: true; + readonly lateral: false; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'post'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + { + readonly ref: { readonly table: 'tag'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo-sqlite/migrations/app/refs/db.contract.json b/examples/prisma-next-demo-sqlite/migrations/app/refs/db.contract.json new file mode 100644 index 0000000000..9461d82c1f --- /dev/null +++ b/examples/prisma-next-demo-sqlite/migrations/app/refs/db.contract.json @@ -0,0 +1,341 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "sql": { + "foreignKeys": true, + "jsonAgg": true, + "limit": true, + "orderBy": true, + "returning": true + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "Post": { + "fields": { + "createdAt": { + "nullable": false, + "type": { "codecId": "sqlite/datetime@1", "kind": "scalar" } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { "length": 36 } + } + }, + "title": { + "nullable": false, + "type": { "codecId": "sqlite/text@1", "kind": "scalar" } + }, + "userId": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { "length": 36 } + } + } + }, + "relations": { + "tags": { + "cardinality": "N:M", + "on": { "localFields": ["id"], "targetFields": ["postId"] }, + "through": { + "childColumns": ["tagId"], + "parentColumns": ["postId"], + "table": "post_tag", + "targetColumns": ["id"] + }, + "to": { "model": "Tag", "namespace": "__unbound__" } + }, + "user": { + "cardinality": "N:1", + "on": { "localFields": ["userId"], "targetFields": ["id"] }, + "to": { "model": "User", "namespace": "__unbound__" } + } + }, + "storage": { + "fields": { + "createdAt": { "column": "createdAt" }, + "id": { "column": "id" }, + "title": { "column": "title" }, + "userId": { "column": "userId" } + }, + "table": "post" + } + }, + "PostTag": { + "fields": { + "postId": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { "length": 36 } + } + }, + "tagId": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { "length": 36 } + } + } + }, + "relations": {}, + "storage": { + "fields": { "postId": { "column": "postId" }, "tagId": { "column": "tagId" } }, + "table": "post_tag" + } + }, + "Tag": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { "length": 36 } + } + }, + "label": { + "nullable": false, + "type": { "codecId": "sqlite/text@1", "kind": "scalar" } + } + }, + "relations": { + "posts": { + "cardinality": "N:M", + "on": { "localFields": ["id"], "targetFields": ["tagId"] }, + "through": { + "childColumns": ["postId"], + "parentColumns": ["tagId"], + "table": "post_tag", + "targetColumns": ["id"] + }, + "to": { "model": "Post", "namespace": "__unbound__" } + } + }, + "storage": { + "fields": { "id": { "column": "id" }, "label": { "column": "label" } }, + "table": "tag" + } + }, + "User": { + "fields": { + "createdAt": { + "nullable": false, + "type": { "codecId": "sqlite/datetime@1", "kind": "scalar" } + }, + "displayName": { + "nullable": false, + "type": { "codecId": "sqlite/text@1", "kind": "scalar" } + }, + "email": { + "nullable": false, + "type": { "codecId": "sqlite/text@1", "kind": "scalar" } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { "length": 36 } + } + } + }, + "relations": { + "posts": { + "cardinality": "1:N", + "on": { "localFields": ["id"], "targetFields": ["userId"] }, + "to": { "model": "Post", "namespace": "__unbound__" } + } + }, + "storage": { + "fields": { + "createdAt": { "column": "createdAt" }, + "displayName": { "column": "displayName" }, + "email": { "column": "email" }, + "id": { "column": "id" } + }, + "table": "user" + } + } + } + } + } + }, + "execution": { + "executionHash": "sha256:70c6ceb0ed79d5888519ec84bf32e31a056283fa8d5c7b5c1ba65b709f8595c8", + "mutations": { + "defaults": [ + { + "onCreate": { "id": "uuidv4", "kind": "generator" }, + "ref": { "column": "id", "table": "post" } + }, + { + "onCreate": { "id": "uuidv4", "kind": "generator" }, + "ref": { "column": "id", "table": "tag" } + }, + { + "onCreate": { "id": "uuidv4", "kind": "generator" }, + "ref": { "column": "id", "table": "user" } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:3cc333ecad9f3f4c7229370a9d2c37e908cdce0f8d2e9fb132d50605b024eff2", + "roots": { + "post": { "model": "Post", "namespace": "__unbound__" }, + "post_tag": { "model": "PostTag", "namespace": "__unbound__" }, + "tag": { "model": "Tag", "namespace": "__unbound__" }, + "user": { "model": "User", "namespace": "__unbound__" } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "tables": { + "post": { + "columns": { + "createdAt": { + "codecId": "sqlite/datetime@1", + "default": { "expression": "now()", "kind": "function" }, + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { "length": 36 } + }, + "title": { "codecId": "sqlite/text@1", "nativeType": "text", "nullable": false }, + "userId": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { "length": 36 } + } + }, + "foreignKeys": [ + { + "constraint": true, + "index": true, + "name": "post_userId_fkey", + "source": { + "columns": ["userId"], + "namespaceId": "__unbound__", + "tableName": "post" + }, + "target": { "columns": ["id"], "namespaceId": "__unbound__", "tableName": "user" } + } + ], + "indexes": [], + "primaryKey": { "columns": ["id"] }, + "uniques": [] + }, + "post_tag": { + "columns": { + "postId": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { "length": 36 } + }, + "tagId": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { "length": 36 } + } + }, + "foreignKeys": [ + { + "constraint": true, + "index": true, + "name": "post_tag_postId_fkey", + "source": { + "columns": ["postId"], + "namespaceId": "__unbound__", + "tableName": "post_tag" + }, + "target": { "columns": ["id"], "namespaceId": "__unbound__", "tableName": "post" } + }, + { + "constraint": true, + "index": true, + "name": "post_tag_tagId_fkey", + "source": { + "columns": ["tagId"], + "namespaceId": "__unbound__", + "tableName": "post_tag" + }, + "target": { "columns": ["id"], "namespaceId": "__unbound__", "tableName": "tag" } + } + ], + "indexes": [], + "primaryKey": { "columns": ["postId", "tagId"], "name": "post_tag_pkey" }, + "uniques": [] + }, + "tag": { + "columns": { + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { "length": 36 } + }, + "label": { "codecId": "sqlite/text@1", "nativeType": "text", "nullable": false } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { "columns": ["id"] }, + "uniques": [] + }, + "user": { + "columns": { + "createdAt": { + "codecId": "sqlite/datetime@1", + "default": { "expression": "now()", "kind": "function" }, + "nativeType": "text", + "nullable": false + }, + "displayName": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + }, + "email": { "codecId": "sqlite/text@1", "nativeType": "text", "nullable": false }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { "length": 36 } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { "columns": ["id"] }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:194cf0bce4a19c7191f9e4ae8670de21707c48678d7b8998d38708a506488c88" + }, + "target": "sqlite", + "targetFamily": "sql" +} diff --git a/examples/prisma-next-demo-sqlite/migrations/app/refs/db.json b/examples/prisma-next-demo-sqlite/migrations/app/refs/db.json new file mode 100644 index 0000000000..90d46ecf92 --- /dev/null +++ b/examples/prisma-next-demo-sqlite/migrations/app/refs/db.json @@ -0,0 +1,4 @@ +{ + "hash": "sha256:194cf0bce4a19c7191f9e4ae8670de21707c48678d7b8998d38708a506488c88", + "invariants": [] +} diff --git a/examples/prisma-next-demo-sqlite/scripts/seed.ts b/examples/prisma-next-demo-sqlite/scripts/seed.ts index 0995fee7f8..ebee198423 100644 --- a/examples/prisma-next-demo-sqlite/scripts/seed.ts +++ b/examples/prisma-next-demo-sqlite/scripts/seed.ts @@ -5,7 +5,8 @@ * * Run with: pnpm seed * - * Creates 2 users (alice, bob) and 3 posts. + * Creates 2 users (alice, bob), 3 posts, 3 tags, and junction rows demonstrating + * the Post ↔ Tag many-to-many relation. * * Prerequisites: * - SQLITE_PATH env var (defaults to ./demo.db) @@ -70,20 +71,73 @@ async function main() { console.log(`Created user: ${alice.email} (id: ${alice.id})`); console.log(`Created user: ${bob.email} (id: ${bob.id})`); - await runtime.execute( + const firstPostRows = await runtime.execute( db.sql.post .insert([{ title: 'First Post', userId: alice.id, createdAt: new Date() }]) + .returning('id', 'title') .build(), ); - await runtime.execute( + const secondPostRows = await runtime.execute( db.sql.post .insert([{ title: 'Second Post', userId: alice.id, createdAt: new Date() }]) + .returning('id', 'title') .build(), ); await runtime.execute( db.sql.post.insert([{ title: 'Third Post', userId: bob.id, createdAt: new Date() }]).build(), ); + const firstPost = firstPostRows[0] ?? null; + const secondPost = secondPostRows[0] ?? null; + if (!firstPost || !secondPost) { + throw new Error('Failed to create posts'); + } + + console.log(`Created post: ${firstPost.title} (id: ${firstPost.id})`); + console.log(`Created post: ${secondPost.title} (id: ${secondPost.id})`); + + const tagTypeScriptRows = await runtime.execute( + db.sql.tag + .insert([{ label: 'typescript' }]) + .returning('id', 'label') + .build(), + ); + const tagOrmRows = await runtime.execute( + db.sql.tag + .insert([{ label: 'orm' }]) + .returning('id', 'label') + .build(), + ); + const tagDemoRows = await runtime.execute( + db.sql.tag + .insert([{ label: 'demo' }]) + .returning('id', 'label') + .build(), + ); + + const tagTypeScript = tagTypeScriptRows[0] ?? null; + const tagOrm = tagOrmRows[0] ?? null; + const tagDemo = tagDemoRows[0] ?? null; + if (!tagTypeScript || !tagOrm || !tagDemo) { + throw new Error('Failed to create tags'); + } + + console.log(`Created tag: ${tagTypeScript.label} (id: ${tagTypeScript.id})`); + console.log(`Created tag: ${tagOrm.label} (id: ${tagOrm.id})`); + console.log(`Created tag: ${tagDemo.label} (id: ${tagDemo.id})`); + + await runtime.execute( + db.sql.post_tag + .insert([ + { postId: firstPost.id, tagId: tagTypeScript.id }, + { postId: firstPost.id, tagId: tagOrm.id }, + { postId: secondPost.id, tagId: tagOrm.id }, + { postId: secondPost.id, tagId: tagDemo.id }, + ]) + .build(), + ); + + console.log('Seeded post_tag junction rows.'); console.log('Seed completed successfully!'); } finally { await runtime.close(); diff --git a/examples/prisma-next-demo-sqlite/src/main.ts b/examples/prisma-next-demo-sqlite/src/main.ts index b42941ead0..9aa6819b6c 100644 --- a/examples/prisma-next-demo-sqlite/src/main.ts +++ b/examples/prisma-next-demo-sqlite/src/main.ts @@ -14,13 +14,28 @@ * each email — single lower(), single beforeCompile(), * repeated execute() * + * Many-to-many commands (Post ↔ Tag via PostTag junction): + * - post-tags Include a post's tags (N:M read) + * - posts-with-tag-some