From 741188f6d999ea3ddfc6d5cb1d4a3475a889fa83 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:03:08 +0200 Subject: [PATCH 01/12] feat(app-cloud): wire @prisma-next/postgres behind a dedicated subpath entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds @prisma-next/postgres (plus its @prisma-next/contract and @prisma-next/sql-contract type-only deps) to @prisma/app-cloud, and gives prisma-next.ts its own tsdown entry so the ./prisma-next subpath export is generated automatically — the index entry never pulls in @prisma-next/* or pg unless a caller explicitly imports the subpath (ADR-0021). Signed-off-by: willbot Signed-off-by: Will Madden --- packages/app-cloud/package.json | 5 + packages/app-cloud/tsdown.config.ts | 2 +- pnpm-lock.yaml | 1029 +++++++++++++++++++++++---- 3 files changed, 901 insertions(+), 135 deletions(-) diff --git a/packages/app-cloud/package.json b/packages/app-cloud/package.json index 8b5389b78..35ca4c935 100644 --- a/packages/app-cloud/package.json +++ b/packages/app-cloud/package.json @@ -5,6 +5,7 @@ "exports": { ".": "./dist/index.mjs", "./control": "./dist/control.mjs", + "./prisma-next": "./dist/prisma-next.mjs", "./package.json": "./package.json" }, "scripts": { @@ -17,10 +18,14 @@ "dependencies": { "@prisma/app": "workspace:0.2.0", "@prisma/alchemy": "workspace:0.2.0", + "@prisma-next/contract": "0.14.0", + "@prisma-next/postgres": "0.14.0", + "@prisma-next/sql-contract": "0.14.0", "alchemy": "2.0.0-beta.59", "effect": "4.0.0-beta.92" }, "devDependencies": { + "@prisma-next/cli": "0.14.0", "@prisma/app-tsdown": "workspace:0.2.0", "@types/bun": "^1.3.13", "tsdown": "^0.15.9", diff --git a/packages/app-cloud/tsdown.config.ts b/packages/app-cloud/tsdown.config.ts index af0526ad3..dea1eda8c 100644 --- a/packages/app-cloud/tsdown.config.ts +++ b/packages/app-cloud/tsdown.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from '@prisma/app-tsdown'; export default defineConfig({ - entry: ['src/index.ts', 'src/control.ts'], + entry: ['src/index.ts', 'src/control.ts', 'src/prisma-next.ts'], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30720a965..19b6742be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: link:systems/storefront alchemy: specifier: 2.0.0-beta.59 - version: 2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0) + version: 2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0) arktype: specifier: ^2.2.3 version: 2.2.3 @@ -163,7 +163,7 @@ importers: version: 1.47.0 alchemy: specifier: 2.0.0-beta.59 - version: 2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0) + version: 2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0) effect: specifier: 4.0.0-beta.92 version: 4.0.0-beta.92 @@ -191,7 +191,7 @@ importers: dependencies: alchemy: specifier: 2.0.0-beta.59 - version: 2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0) + version: 2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0) effect: specifier: 4.0.0-beta.92 version: 4.0.0-beta.92 @@ -210,7 +210,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)) + version: 4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) packages/app-assemble: dependencies: @@ -267,6 +267,15 @@ importers: packages/app-cloud: dependencies: + '@prisma-next/contract': + specifier: 0.14.0 + version: 0.14.0(typescript@6.0.3) + '@prisma-next/postgres': + specifier: 0.14.0 + version: 0.14.0(typanion@3.14.0)(typescript@6.0.3) + '@prisma-next/sql-contract': + specifier: 0.14.0 + version: 0.14.0(typescript@6.0.3) '@prisma/alchemy': specifier: workspace:0.2.0 version: link:../alchemy @@ -275,11 +284,14 @@ importers: version: link:../app alchemy: specifier: 2.0.0-beta.59 - version: 2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0) + version: 2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0) effect: specifier: 4.0.0-beta.92 version: 4.0.0-beta.92 devDependencies: + '@prisma-next/cli': + specifier: 0.14.0 + version: 0.14.0(typanion@3.14.0)(typescript@6.0.3) '@prisma/app-tsdown': specifier: workspace:0.2.0 version: link:../tsdown-config @@ -294,7 +306,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)) + version: 4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) packages/app-nextjs: dependencies: @@ -366,7 +378,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)) + version: 4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) packages/prisma-app: dependencies: @@ -755,158 +767,158 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@esbuild/aix-ppc64@0.27.0': - resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.0': - resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.0': - resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.0': - resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.0': - resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.0': - resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.0': - resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': - resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.0': - resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.0': - resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.0': - resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.0': - resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.0': - resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.0': - resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.0': - resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.0': - resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.0': - resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.0': - resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': - resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.0': - resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': - resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.0': - resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.0': - resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.0': - resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.0': - resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.0': - resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1211,6 +1223,10 @@ packages: cpu: [x64] os: [win32] + '@noble/hashes@2.2.0': + resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + engines: {node: '>= 20.19.0'} + '@nodable/entities@2.2.0': resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} @@ -1298,6 +1314,286 @@ packages: '@oxc-project/types@0.95.0': resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==} + '@prisma-next/adapter-postgres@0.14.0': + resolution: {integrity: sha512-c90KFtI74WU+8YH3uhVg1DTmD10T2BpINzzys9F3n1jTM2Am0tXKx53V05Ev+7H1VBrjBoksZPZ92WWxVEXLew==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/cli-telemetry@0.14.0': + resolution: {integrity: sha512-JaP/+09tIL6zH+5WQS507db2HQ0B47HRR2AZD8VHmsw/tMwBDZqKVdIA5KptPMVSR3pcnXDygGvG7E8N+PSPRg==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/cli@0.14.0': + resolution: {integrity: sha512-wN4mZc0K1m6wB4NXSEdrA2RCfc8zHBs5yJq0G7XoWQ8J8vvX/hR2kkukJv9QfEYFEIMyOT7vjbSBh2vj4NQ56g==} + engines: {node: '>=24'} + hasBin: true + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/config@0.14.0': + resolution: {integrity: sha512-7U4Y5GeicvWy0dcg78J8arzqe13r97Zj8yB/e+Uea6M8LZxV2poW/afHlYnU0VXhLTZ4F29CdlWOUmCAKpl9mg==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/contract-authoring@0.14.0': + resolution: {integrity: sha512-79aLJO04p/XRX2QvIlFMR2ZvbqvI0xFAU5idgYySoevq8bSY9ORGA0XchSbhFl0d6d1VfSEZowRPwzvgl/6lsg==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/contract@0.14.0': + resolution: {integrity: sha512-HMeduqWGl1NpRyhEvsswF1T8PSBPfbpvMNYMegiYPt2ck5oClhbl0LHkgAIHsOjBg3IJv2VhRqDhQiuRVbkC4A==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/driver-postgres@0.14.0': + resolution: {integrity: sha512-qla7/fAk2YqwomkUPevd9bIqJ5gQZcZTXbEoz8nJaErUunisEosepJOKwlTpquJV2+OAuezODCbrrYtUJ525jg==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/emitter@0.14.0': + resolution: {integrity: sha512-qdyOe11AsXHMpVmPYulA/NjKEn59VkXekGrtg8lB+Pqd+Dg2HwQinaO6G1PXjymRyVgv31UKRyUU7eLhCQaZ0Q==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/errors@0.14.0': + resolution: {integrity: sha512-+VlDHQaP3AAs3rYYYSDoG5Ng5Hkyl+F4oAHAvDsvhFMupsGWaBx7esc5MbBHfnjE/5i/cSybR8/efXrIZJI6zA==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/family-sql@0.14.0': + resolution: {integrity: sha512-Gx4ByuilDiJzwsOkqWE42CSZrhO9/6uFDpyemD185eZsaJGk5tvEpbLy3UuLNx+6VEsvXfPYGLdGUy2LL8e9Pg==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/framework-components@0.14.0': + resolution: {integrity: sha512-6ZPnesKake4pnRRQZUi67a5qfgs6Pc9cmNN2bAf2bQJYTg3w5b1yOhjg2sFupXT54hq/iE3rXRY5A7ltgSxijA==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/ids@0.14.0': + resolution: {integrity: sha512-E+Nhvgmco/Y24wiv/hWVt6wAf1J9sIS5qupoolBeL0V/BA/+zECewg/vCF2dj92hXk0jJ1WphhQDGrvx5XAPbQ==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/migration-tools@0.14.0': + resolution: {integrity: sha512-JdFJPO8qFA3N0lP6g/ZsRz4QGeDV/qQpRUWq+9phoAbL/kBaMha838JG5YUb6h8z3tD4/qg+Qs6ESZPURD4AcQ==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/operations@0.14.0': + resolution: {integrity: sha512-h53frkeEYalkixESJvkCvvuELD5b3YnHdkqkBK1A6XyDoqFusEHpjlP4LMXhpAQlft6QdbgYZfttm9ZXBFBUQw==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/postgres@0.14.0': + resolution: {integrity: sha512-0TXr3+x3INseRhzPUmMn2t6NUhD3DlDht0MHJ8SBvSpkiGXH237IwID161KtZ7xa6ihTTXdjDfIqgmbDtVxfEQ==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/psl-parser@0.14.0': + resolution: {integrity: sha512-XKumV4Bfy3KHoD3IsWVSDtzFjXKL5zENASbbfIXXpwll3AVgU4rtVKuJIH5fwUOYpcCUdLE8ZaNw6vcwLuse3w==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/psl-printer@0.14.0': + resolution: {integrity: sha512-qVTjwpO9uvITSsMg/mtdk0Py7ITsQRpPjmidSi9ByoPE6OGBGF3/BaUbOjRiZ340i39Npg83jT4rLreH1GOFjg==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-builder@0.14.0': + resolution: {integrity: sha512-11AAASmhf7CZ4M/qQ5JHaHxHl+olBf5TP6jYSKuX8c9COttyp9IN9QRI0+PKR0QSjZgU1VX9WPNRG8hEcmDrZw==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-contract-emitter@0.14.0': + resolution: {integrity: sha512-gUgctyFNMKZ94wXNATevMi+FG9R9SK4yYQesYr7/5kZ4ZBbHkaFOFcWwlJZSI+EhCC2J1OgvfaCTtpWpn+VIUw==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-contract-psl@0.14.0': + resolution: {integrity: sha512-86Z8MWo0NYhGSX4uOqbvAx46aMOaB2opNyUeFNnntT1br+U48IX2ZPGV62ul6mwCp7DGN7pjvmlBW5OnHXuiyg==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-contract-ts@0.14.0': + resolution: {integrity: sha512-AMTURIMXCsn+M7oW0QOe/9wS7J+Q1siW7+aihgtmBj88lYav9XHyxJO5AJDu6LSDSctorTzccfsPatBAlcxzKQ==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-contract@0.14.0': + resolution: {integrity: sha512-/Ibvjd41sEgpi4c3YCZvK6vRCucNffsIZ+isIOYoVqXOt74H0llM4Ux4mR0PShNuo0jgG8xOon1ptSQ6/S7c+w==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-errors@0.14.0': + resolution: {integrity: sha512-zc5U+SzEUqpjPC69w9JejqAkcRtkZ8BSm1a6l6p4pT4b8dk/iXrfQ96oHoy6SYG/dTbGe47W4gCfX9LCzCdUlA==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-operations@0.14.0': + resolution: {integrity: sha512-0VeVceyh7LAlEGMseFPQrmoBPkNAlBiCOCyWzv1xQjjU0IoPkMlcjFu07wD5aLf+nwBzEUSbdg9EaTdBTYHO9Q==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-orm-client@0.14.0': + resolution: {integrity: sha512-fXyiR/8r3NqLqUKTxrL0o5KcxHW6r91pD0pU1tmnB6kJD3MS++Fhd2miDcbCn/5O0qf71zfmJDafKU74TOJ3dw==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-relational-core@0.14.0': + resolution: {integrity: sha512-7H1HaN31lmGD81C9vz07dq/r5n80yKXfFNpYj5XrKKeVpDZQSb/IXtOVt7LyB+q9QqSUW2jD5RGLuTOhsd2kbw==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-runtime@0.14.0': + resolution: {integrity: sha512-HV8ZAvNuXtFrAHuxs3/ZEz0JapKFayFYDljQeVDFxExSEj79fPtd9CON9sTTV4T/RKII5knlKJRuKtwWH35Znw==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/sql-schema-ir@0.14.0': + resolution: {integrity: sha512-5buE3jcTz3yyyve4demaACIv52YT09tFPCBEqwmxG1ss8KtybVEgaZg8UUYGmyYbWU0l5RpQ96/vuB5UTFE4nw==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/target-postgres@0.14.0': + resolution: {integrity: sha512-i1H9Jzt7ZGgYQ61rlmalyEPY1RoLFIMk9+qE5WbETjQ8LY9p7iXeTx4bBv25twkYIAHxXtrYxsQ3klUWg0JfVg==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/ts-render@0.14.0': + resolution: {integrity: sha512-ub25pEfUe2dLpNu/wR0mlBBMZ87eGmyV5D9GS5KmyHkofjJdiE2Y3BpIhHXf7wjiYp35z/gOyQ2vuPFS4xVBgQ==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + + '@prisma-next/utils@0.14.0': + resolution: {integrity: sha512-YIV6FMdKKsJqLkG2h9tQDzEoeAFjtniNXcB0XnyZyD1tkALodiWsRymuQJBGcFZB7S8icrOvRaqdSUNdrrR6yw==} + engines: {node: '>=24'} + peerDependencies: + typescript: '>=5.9' + peerDependenciesMeta: + typescript: + optional: true + '@prisma/management-api-sdk@1.47.0': resolution: {integrity: sha512-h4h3obs0F7WFUjNhh3vU7de7aESywu8RjO7KNO5/DXZ8QewLmeUjx65nUbQPxcRgDana6c79r5yPRCXSMchfeQ==} @@ -1808,6 +2104,10 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -1832,6 +2132,14 @@ packages: peerDependencies: typanion: '*' + clipanion@4.0.0-rc.4: + resolution: {integrity: sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==} + peerDependencies: + typanion: '*' + + closest-match@1.3.3: + resolution: {integrity: sha512-RSdHrZwNOvt2uMQgqJDJdM/I+5MlJ1tQJEXYrbRjSMXWiCRo06g2hwObJ7+WKt2J9ySK9/pJ0Q2vbL+BPkofDA==} + cluster-key-slot@1.1.1: resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} @@ -1931,8 +2239,8 @@ packages: es-toolkit@1.49.0: resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} - esbuild@0.27.0: - resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -2107,6 +2415,9 @@ packages: json-with-bigint@3.5.8: resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -2315,6 +2626,9 @@ packages: openapi-typescript-helpers@0.0.15: resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + package-manager-detector@1.7.0: + resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -2338,6 +2652,11 @@ packages: pg-connection-string@2.14.0: resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + pg-cursor@2.21.0: + resolution: {integrity: sha512-IYvk/j+Suhtbo/C3uOf4JLsLK/gWxOTUOmYbDsbKnLaVJDq+KwhwK6ngpRfiCk8eDMS3AmGQABZCv0cREEzHQw==} + peerDependencies: + pg: ^8 + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -2354,6 +2673,15 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} + pg@8.21.0: + resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + pg@8.22.0: resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} engines: {node: '>= 16.0.0'} @@ -2412,6 +2740,11 @@ packages: resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} engines: {node: '>=12'} + prettier@3.9.5: + resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} + engines: {node: '>=14'} + hasBin: true + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -2661,6 +2994,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + tsdown@0.15.12: resolution: {integrity: sha512-c8VLlQm8/lFrOAg5VMVeN4NAbejZyVQkzd+ErjuaQgJFI/9MhR9ivr0H/CM7UlOF1+ELlF6YaI7sU/4itgGQ8w==} engines: {node: '>=20.19.0'} @@ -2755,6 +3091,10 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + uniku@0.0.12: + resolution: {integrity: sha512-wqt0D/ZcBTDprQxlFpxDm4jCHlQyHWQ62PMXPF0AfU7oxHm4g5++7EY7/+uwApCMOhfzRE8fTf4WATe6lH1vkg==} + engines: {node: '>=24.13.0'} + universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} @@ -2863,6 +3203,10 @@ packages: engines: {node: '>=16'} hasBin: true + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -3214,14 +3558,14 @@ snapshots: '@distilled.cloud/core': 0.27.0(effect@4.0.0-beta.92) effect: 4.0.0-beta.92 - '@distilled.cloud/cloudflare-rolldown-plugin@0.11.3(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1)': + '@distilled.cloud/cloudflare-rolldown-plugin@0.11.3(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1)': dependencies: '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260617.1) magic-string: 0.30.21 unenv: 2.0.0-rc.24 optionalDependencies: rolldown: 1.0.1 - vite: 8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) transitivePeerDependencies: - workerd @@ -3235,13 +3579,13 @@ snapshots: '@effect/platform-bun': 4.0.0-beta.92(effect@4.0.0-beta.92) '@effect/platform-node': 4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1) - '@distilled.cloud/cloudflare-vite-plugin@0.11.3(@distilled.cloud/cloudflare-runtime@0.11.3(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92))(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92)(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1)': + '@distilled.cloud/cloudflare-vite-plugin@0.11.3(@distilled.cloud/cloudflare-runtime@0.11.3(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92))(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92)(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1)': dependencies: '@distilled.cloud/cloudflare': 0.27.0(effect@4.0.0-beta.92) - '@distilled.cloud/cloudflare-rolldown-plugin': 0.11.3(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1) + '@distilled.cloud/cloudflare-rolldown-plugin': 0.11.3(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1) '@distilled.cloud/cloudflare-runtime': 0.11.3(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92) effect: 4.0.0-beta.92 - vite: 8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) optionalDependencies: '@effect/platform-bun': 4.0.0-beta.92(effect@4.0.0-beta.92) '@effect/platform-node': 4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1) @@ -3297,10 +3641,10 @@ snapshots: - utf-8-validate optional: true - '@effect/vitest@4.0.0-beta.92(effect@4.0.0-beta.92)(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)))': + '@effect/vitest@4.0.0-beta.92(effect@4.0.0-beta.92)(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))': dependencies: effect: 4.0.0-beta.92 - vitest: 4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@emnapi/core@1.10.0': dependencies: @@ -3334,82 +3678,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.0': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.0': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.0': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.0': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.0': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.0': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.0': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.0': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.0': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.0': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.0': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.0': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.0': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.0': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.0': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.0': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.0': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.0': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.0': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.0': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.0': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.0': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.0': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.0': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.0': + '@esbuild/win32-x64@0.28.1': optional: true '@img/colour@1.1.0': @@ -3644,6 +3988,8 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.19': optional: true + '@noble/hashes@2.2.0': {} + '@nodable/entities@2.2.0': {} '@nodelib/fs.scandir@2.1.5': @@ -3737,6 +4083,378 @@ snapshots: '@oxc-project/types@0.95.0': {} + '@prisma-next/adapter-postgres@0.14.0(typanion@3.14.0)(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/contract-authoring': 0.14.0(typescript@6.0.3) + '@prisma-next/errors': 0.14.0(typescript@6.0.3) + '@prisma-next/family-sql': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/ids': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract-psl': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract-ts': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-relational-core': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-runtime': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-schema-ir': 0.14.0(typescript@6.0.3) + '@prisma-next/target-postgres': 0.14.0(typanion@3.14.0)(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - magicast + - typanion + + '@prisma-next/cli-telemetry@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/config': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + c12: 3.3.4 + pathe: 2.0.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - magicast + + '@prisma-next/cli@0.14.0(typanion@3.14.0)(typescript@6.0.3)': + dependencies: + '@clack/prompts': 1.6.0 + '@prisma-next/cli-telemetry': 0.14.0(typescript@6.0.3) + '@prisma-next/config': 0.14.0(typescript@6.0.3) + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/emitter': 0.14.0(typescript@6.0.3) + '@prisma-next/errors': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/migration-tools': 0.14.0(typescript@6.0.3) + '@prisma-next/psl-printer': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + c12: 3.3.4 + ci-info: 4.4.0 + clipanion: 4.0.0-rc.4(typanion@3.14.0) + closest-match: 1.3.3 + colorette: 2.0.20 + commander: 14.0.3 + esbuild: 0.28.1 + jsonc-parser: 3.3.1 + package-manager-detector: 1.7.0 + pathe: 2.0.3 + string-width: 8.2.1 + strip-ansi: 7.2.0 + wrap-ansi: 10.0.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - magicast + - typanion + + '@prisma-next/config@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/contract-authoring@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/contract@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + '@standard-schema/spec': 1.1.0 + arktype: 2.2.3 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/driver-postgres@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/errors': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-errors': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-relational-core': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + pg: 8.21.0 + pg-cursor: 2.21.0(pg@8.21.0) + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - pg-native + + '@prisma-next/emitter@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/operations': 0.14.0(typescript@6.0.3) + '@prisma-next/ts-render': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + prettier: 3.9.5 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/errors@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/family-sql@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/emitter': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/migration-tools': 0.14.0(typescript@6.0.3) + '@prisma-next/operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract-emitter': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract-ts': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-relational-core': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-runtime': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-schema-ir': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/framework-components@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/operations': 0.14.0(typescript@6.0.3) + '@prisma-next/ts-render': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + '@standard-schema/spec': 1.1.0 + arktype: 2.2.3 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/ids@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + uniku: 0.0.12 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/migration-tools@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + pathe: 2.0.3 + prettier: 3.9.5 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/operations@0.14.0(typescript@6.0.3)': + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/postgres@0.14.0(typanion@3.14.0)(typescript@6.0.3)': + dependencies: + '@prisma-next/adapter-postgres': 0.14.0(typanion@3.14.0)(typescript@6.0.3) + '@prisma-next/cli': 0.14.0(typanion@3.14.0)(typescript@6.0.3) + '@prisma-next/config': 0.14.0(typescript@6.0.3) + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/driver-postgres': 0.14.0(typescript@6.0.3) + '@prisma-next/family-sql': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-builder': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract-psl': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract-ts': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-orm-client': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-relational-core': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-runtime': 0.14.0(typescript@6.0.3) + '@prisma-next/target-postgres': 0.14.0(typanion@3.14.0)(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + pathe: 2.0.3 + pg: 8.21.0 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - magicast + - pg-native + - typanion + + '@prisma-next/psl-parser@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/psl-printer@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-builder@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-relational-core': 0.14.0(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-contract-emitter@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/emitter': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-contract-psl@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/config': 0.14.0(typescript@6.0.3) + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/psl-parser': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract-ts': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + pathe: 2.0.3 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-contract-ts@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/config': 0.14.0(typescript@6.0.3) + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/contract-authoring': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + pathe: 2.0.3 + ts-toolbelt: 9.6.0 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-contract@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-errors@0.14.0(typescript@6.0.3)': + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-operations@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-orm-client@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-errors': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-relational-core': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-runtime': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-relational-core@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-operations': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + '@standard-schema/spec': 1.1.0 + arktype: 2.2.3 + ts-toolbelt: 9.6.0 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-runtime@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/ids': 0.14.0(typescript@6.0.3) + '@prisma-next/operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-relational-core': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + arktype: 2.2.3 + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/sql-schema-ir@0.14.0(typescript@6.0.3)': + dependencies: + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/target-postgres@0.14.0(typanion@3.14.0)(typescript@6.0.3)': + dependencies: + '@prisma-next/cli': 0.14.0(typanion@3.14.0)(typescript@6.0.3) + '@prisma-next/contract': 0.14.0(typescript@6.0.3) + '@prisma-next/errors': 0.14.0(typescript@6.0.3) + '@prisma-next/family-sql': 0.14.0(typescript@6.0.3) + '@prisma-next/framework-components': 0.14.0(typescript@6.0.3) + '@prisma-next/migration-tools': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-contract': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-errors': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-operations': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-relational-core': 0.14.0(typescript@6.0.3) + '@prisma-next/sql-schema-ir': 0.14.0(typescript@6.0.3) + '@prisma-next/ts-render': 0.14.0(typescript@6.0.3) + '@prisma-next/utils': 0.14.0(typescript@6.0.3) + '@standard-schema/spec': 1.1.0 + arktype: 2.2.3 + pathe: 2.0.3 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - magicast + - typanion + + '@prisma-next/ts-render@0.14.0(typescript@6.0.3)': + optionalDependencies: + typescript: 6.0.3 + + '@prisma-next/utils@0.14.0(typescript@6.0.3)': + optionalDependencies: + typescript: 6.0.3 + '@prisma/management-api-sdk@1.47.0': dependencies: openapi-fetch: 0.14.0 @@ -4007,13 +4725,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.9(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) '@vitest/pretty-format@4.1.9': dependencies: @@ -4039,7 +4757,7 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - alchemy@2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0): + alchemy@2.0.0-beta.59(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(@types/node@24.10.13)(@types/react@19.2.14)(effect@4.0.0-beta.92)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)))(workerd@1.20260617.1)(ws@8.21.0): dependencies: '@alchemy.run/node-utils': 0.0.5 '@aws-sdk/credential-providers': 3.1077.0 @@ -4047,13 +4765,13 @@ snapshots: '@distilled.cloud/aws': 0.27.0(effect@4.0.0-beta.92) '@distilled.cloud/axiom': 0.27.0(effect@4.0.0-beta.92) '@distilled.cloud/cloudflare': 0.27.0(effect@4.0.0-beta.92) - '@distilled.cloud/cloudflare-rolldown-plugin': 0.11.3(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1) + '@distilled.cloud/cloudflare-rolldown-plugin': 0.11.3(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1) '@distilled.cloud/cloudflare-runtime': 0.11.3(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92) - '@distilled.cloud/cloudflare-vite-plugin': 0.11.3(@distilled.cloud/cloudflare-runtime@0.11.3(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92))(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92)(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1) + '@distilled.cloud/cloudflare-vite-plugin': 0.11.3(@distilled.cloud/cloudflare-runtime@0.11.3(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92))(@distilled.cloud/cloudflare@0.27.0(effect@4.0.0-beta.92))(@effect/platform-bun@4.0.0-beta.92(effect@4.0.0-beta.92))(@effect/platform-node@4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1))(effect@4.0.0-beta.92)(rolldown@1.0.1)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))(workerd@1.20260617.1) '@distilled.cloud/core': 0.27.0(effect@4.0.0-beta.92) '@distilled.cloud/neon': 0.27.0(effect@4.0.0-beta.92) '@distilled.cloud/planetscale': 0.27.0(effect@4.0.0-beta.92) - '@effect/vitest': 4.0.0-beta.92(effect@4.0.0-beta.92)(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0))) + '@effect/vitest': 4.0.0-beta.92(effect@4.0.0-beta.92)(vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0))) '@libsql/client': 0.17.4 '@octokit/rest': 22.0.1 '@octokit/webhooks': 14.2.0 @@ -4081,7 +4799,7 @@ snapshots: optionalDependencies: '@effect/platform-bun': 4.0.0-beta.92(effect@4.0.0-beta.92) '@effect/platform-node': 4.0.0-beta.92(effect@4.0.0-beta.92)(ioredis@5.11.1) - vite: 8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) ws: 8.21.0 transitivePeerDependencies: - '@types/node' @@ -4175,6 +4893,8 @@ snapshots: dependencies: readdirp: 5.0.0 + ci-info@4.4.0: {} + cli-boxes@3.0.0: {} cli-cursor@4.0.0: @@ -4196,6 +4916,12 @@ snapshots: dependencies: typanion: 3.14.0 + clipanion@4.0.0-rc.4(typanion@3.14.0): + dependencies: + typanion: 3.14.0 + + closest-match@1.3.3: {} + cluster-key-slot@1.1.1: optional: true @@ -4262,35 +4988,34 @@ snapshots: es-toolkit@1.49.0: {} - esbuild@0.27.0: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.0 - '@esbuild/android-arm': 0.27.0 - '@esbuild/android-arm64': 0.27.0 - '@esbuild/android-x64': 0.27.0 - '@esbuild/darwin-arm64': 0.27.0 - '@esbuild/darwin-x64': 0.27.0 - '@esbuild/freebsd-arm64': 0.27.0 - '@esbuild/freebsd-x64': 0.27.0 - '@esbuild/linux-arm': 0.27.0 - '@esbuild/linux-arm64': 0.27.0 - '@esbuild/linux-ia32': 0.27.0 - '@esbuild/linux-loong64': 0.27.0 - '@esbuild/linux-mips64el': 0.27.0 - '@esbuild/linux-ppc64': 0.27.0 - '@esbuild/linux-riscv64': 0.27.0 - '@esbuild/linux-s390x': 0.27.0 - '@esbuild/linux-x64': 0.27.0 - '@esbuild/netbsd-arm64': 0.27.0 - '@esbuild/netbsd-x64': 0.27.0 - '@esbuild/openbsd-arm64': 0.27.0 - '@esbuild/openbsd-x64': 0.27.0 - '@esbuild/openharmony-arm64': 0.27.0 - '@esbuild/sunos-x64': 0.27.0 - '@esbuild/win32-arm64': 0.27.0 - '@esbuild/win32-ia32': 0.27.0 - '@esbuild/win32-x64': 0.27.0 - optional: true + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escape-string-regexp@2.0.0: {} @@ -4464,6 +5189,8 @@ snapshots: json-with-bigint@3.5.8: {} + jsonc-parser@3.3.1: {} + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -4679,6 +5406,8 @@ snapshots: openapi-typescript-helpers@0.0.15: {} + package-manager-detector@1.7.0: {} + pako@1.0.11: {} patch-console@2.0.0: {} @@ -4694,8 +5423,16 @@ snapshots: pg-connection-string@2.14.0: {} + pg-cursor@2.21.0(pg@8.21.0): + dependencies: + pg: 8.21.0 + pg-int8@1.0.1: {} + pg-pool@3.14.0(pg@8.21.0): + dependencies: + pg: 8.21.0 + pg-pool@3.14.0(pg@8.22.0): dependencies: pg: 8.22.0 @@ -4710,6 +5447,16 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 + pg@8.21.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.21.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + pg@8.22.0: dependencies: pg-connection-string: 2.14.0 @@ -4762,6 +5509,8 @@ snapshots: postgres@3.4.9: {} + prettier@3.9.5: {} + process-nextick-args@2.0.1: {} promise-limit@2.7.0: {} @@ -5047,6 +5796,8 @@ snapshots: tree-kill@1.2.2: {} + ts-toolbelt@9.6.0: {} + tsdown@0.15.12(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(typescript@6.0.3): dependencies: ansis: 4.3.1 @@ -5135,13 +5886,17 @@ snapshots: dependencies: pathe: 2.0.3 + uniku@0.0.12: + dependencies: + '@noble/hashes': 2.2.0 + universal-user-agent@7.0.3: {} util-deprecate@1.0.2: {} uuid@14.0.1: {} - vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0): + vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -5150,15 +5905,15 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.10.13 - esbuild: 0.27.0 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.6.1 yaml: 2.9.0 - vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)): + vitest@4.1.9(@types/node@24.10.13)(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.9(vite@8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.9 '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 @@ -5175,7 +5930,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.2(@types/node@24.10.13)(esbuild@0.27.0)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.1.2(@types/node@24.10.13)(esbuild@0.28.1)(jiti@2.6.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.13 @@ -5199,6 +5954,12 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260617.1 '@cloudflare/workerd-windows-64': 1.20260617.1 + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.1 + strip-ansi: 7.2.0 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 From b6d74480fa69230b7c6e440826fae7425f82a954 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:03:15 +0200 Subject: [PATCH 02/12] feat(app-cloud): add the pnPostgres Prisma Next-typed postgres primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second, parallel data primitive alongside bare postgres() (untouched): pnPostgres(), overloaded like rpc()/postgres() into a resource end ({ name, config }, providing config.contract) and a dependency end (pnPostgres(contract), binding to the typed Prisma Next client). The prisma-next Contract kind's __cmp carries the deserialized contract data plus a type-only _contract anchor pinning the emitted contract's branded storageHash literal, so plain TypeScript assignability between two wrapped contracts is exact-version equality; satisfies() mirrors it at Load with a real storageHash comparison. Hydrate constructs the client via @prisma-next/postgres/runtime with verifyMarker: 'onFirstUse', which is natively warn-only (confirmed by reading sql-runtime's verifyMarker() implementation, not just its .d.ts) — a marker mismatch logs and returns, it never throws, so no wrapper is needed to satisfy "the app always attempts to run" (ADR-0021). Signed-off-by: willbot Signed-off-by: Will Madden --- packages/app-cloud/src/prisma-next.ts | 189 ++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 packages/app-cloud/src/prisma-next.ts diff --git a/packages/app-cloud/src/prisma-next.ts b/packages/app-cloud/src/prisma-next.ts new file mode 100644 index 000000000..d9720bac8 --- /dev/null +++ b/packages/app-cloud/src/prisma-next.ts @@ -0,0 +1,189 @@ +/** + * `pnPostgres()` is the `prisma-next` kind's single entry, overloaded by what + * it's given — the typed sibling of bare `postgres()` (untouched in + * `postgres.ts`). A resource end takes `{ name, config }`, where + * `config.contract` anchors the resource's provided contract; a dependency + * end takes that same wrapped contract value and hydrates to a typed Prisma + * Next client, `Client`, over the injected `{ url }` connection. + * This amends ADR-0015 for this one dep kind (see ADR-0021): Prisma Next is + * framework-blessed like rpc, so the binding is the typed client itself, not + * a config the app builds its own client from. + * + * `Cmp` (`PnCmp`) carries the deserialized contract data (`contractJson`, + * read by hydrate) plus a type-only `_contract` anchor pinning the emitted + * contract's branded `storageHash` literal — the lever that makes plain + * TypeScript assignability between two wrapped contracts exact-version + * equality. `satisfies()` mirrors it at Load with a real `storageHash` + * comparison (not `rpc`'s identity check — this kind needs a genuine + * version-equality test, since two distinct in-memory contract values can + * carry the same hash). + * + * Marker verification is warn-only by construction, not by a wrapper here: + * Prisma Next's own `verifyMarker: 'onFirstUse'` never throws on a mismatch + * — confirmed by reading `@prisma-next/sql-runtime`'s `verifyMarker()` + * implementation (not just its `.d.ts`): a missing or mismatched marker logs + * `CONTRACT.MARKER_MISSING` / `CONTRACT.MARKER_MISMATCH` and returns. The one + * gap: `@prisma-next/postgres@0.14.0`'s public `postgres()` options don't + * forward a `log` sink to the runtime, so that warning lands in Prisma + * Next's internal no-op logger and isn't visible anywhere by default in this + * version — a Prisma Next limitation, not something hydrate can route around + * without depending on unexported internals or querying the DB at hydrate + * (which would break the lazy-pool contract). The "never throws" half of the + * requirement holds regardless. + */ + +import type { Contract, DependencyEnd, ResourceNode } from '@prisma/app'; +import { dependency, resource } from '@prisma/app'; +import type { Contract as PnRawContract } from '@prisma-next/contract/types'; +import pnPostgresRuntime, { type PostgresClient } from '@prisma-next/postgres/runtime'; +import type { SqlStorage } from '@prisma-next/sql-contract/types'; + +/** + * Any Prisma Next contract this primitive can carry — the bound both + * authoring modes (TS no-emit `defineContract()`, or PSL/emitted + * `contract.d.ts`) satisfy. + */ +export type AnyPnContract = PnRawContract; + +/** + * The comparison payload behind a `prisma-next` Contract. `contractJson` is + * the plain, deserialized contract data hydrate hands the Prisma Next + * runtime — the same shape as an emitted `contract.json`, or a TS-authored + * contract's own value (both satisfy `postgres()`'s `contractJson: unknown` + * parameter). `_contract` never exists at runtime — it is the type-only + * anchor that carries the emitted contract's type, so plain assignability + * between two `PnCmp`s requires `_contract`'s type to match, which for a + * branded `storageHash` literal means the hashes must be identical. + */ +export interface PnCmp { + readonly contractJson: unknown; + readonly _contract?: C; +} + +/** The `prisma-next` kind: a Contract whose `Cmp` is `PnCmp`. */ +export type PnPostgresContract = Contract< + 'prisma-next', + PnCmp +>; + +/** Recovers the emitted contract type `C` a `prisma-next` Contract carries. */ +export type PnContractOf = Ct extends PnPostgresContract ? C : never; + +/** The typed client a consumer's `pnPostgres(contract)` dependency hydrates to. */ +export type Client = PostgresClient>; + +/** + * The resource end's config — the framework's own thin wrapper carrying the + * wrapped `prisma-next` contract this database provides. Deliberately NOT + * `@prisma-next/postgres/config`'s own `PrismaNextConfig`: that type anchors + * Prisma Next's OWN CLI (`contract emit` / `migrate`) to a + * `prisma-next.config.ts` file on disk, which is a separate, parallel + * artifact this dispatch does not read (the deploy lowering that will read + * it is slice 2's job). `connection` is accepted only to mirror the shape + * the design sketched and is always ignored — the framework injects the + * connection URL at hydrate (no-globals). + */ +export interface PnPostgresConfig { + readonly contract: C; + readonly connection?: unknown; +} + +/** + * Wraps a resolved Prisma Next contract value into the framework's + * `prisma-next` Contract kind. Both the resource end's `config.contract` and + * every dependency end reference the SAME wrapped value, the same way + * `@prisma/app-rpc`'s `contract()` output feeds both a service's `expose` + * and its consumers' `rpc(contract)` deps. + * + * Two overloads, mirroring `@prisma-next/postgres/runtime`'s own + * `PostgresOptionsWithContract` / `PostgresOptionsWithContractJson` split: + * - TS-authored, no-emit (`defineContract()`'s own return value): `C` is + * inferred from the argument, which already carries the branded types. + * - Emitted (a deserialized `contract.json`, e.g. via a JSON module import): + * the argument's inferred type is plain JSON data, not the branded + * `contract.d.ts` type, so `C` cannot be inferred from it — pass it + * explicitly: `pnContract(contractJson)`. + */ +export function pnContract(contract: C): PnPostgresContract; +export function pnContract(contractJson: unknown): PnPostgresContract; +export function pnContract(contract: unknown): unknown { + const value: PnPostgresContract = { + kind: 'prisma-next', + __cmp: { contractJson: contract }, + satisfies: (required) => { + const requiredHash = storageHashOf(required); + return requiredHash !== undefined && requiredHash === storageHashOf(value); + }, + }; + return Object.freeze(value); +} + +/** + * `{ name, config }` — the resource identity a system provisions: the ONE + * place the database exists, providing `config.contract`. + */ +export function pnPostgres(opts: { + name: string; + config: PnPostgresConfig; +}): ResourceNode; +/** + * `pnPostgres(contract)` — a service's dependency on a Prisma Next-typed + * Postgres. Its binding is the typed Prisma Next client, constructed by the + * framework in hydrate from the contract plus the injected connection URL. + */ +export function pnPostgres(contract: C): DependencyEnd, C>; +export function pnPostgres( + arg: { name: string; config: PnPostgresConfig } | PnPostgresContract, +): unknown { + if (!isPnPostgresContract(arg)) { + return resource({ + name: arg.name, + extension: '@prisma/app-cloud', + provides: arg.config.contract, + }); + } + const contract = arg; + return dependency<{ url: { type: 'string'; secret: true } }, unknown, PnPostgresContract>({ + type: 'prisma-next', + connection: { + params: { url: { type: 'string', secret: true } }, + hydrate: ({ url }) => buildClient(contract, url), + }, + required: contract, + }); +} + +function isPnPostgresContract(value: unknown): value is PnPostgresContract { + return ( + typeof value === 'object' && + value !== null && + 'kind' in value && + value.kind === 'prisma-next' && + '__cmp' in value && + 'satisfies' in value + ); +} + +function buildClient(contract: C, url: string): Client { + return pnPostgresRuntime>({ + contractJson: contract.__cmp.contractJson, + url, + verifyMarker: 'onFirstUse', + }); +} + +/** Reads `__cmp.contractJson.storage.storageHash` off a `prisma-next` Contract, defensively — `__cmp` is opaque to core, so nothing guarantees its shape without a runtime check. */ +function storageHashOf(contract: Contract<'prisma-next', unknown> | undefined): string | undefined { + if (contract === undefined) return undefined; + const cmp = contract.__cmp; + if (typeof cmp !== 'object' || cmp === null || !('contractJson' in cmp)) return undefined; + const contractJson = cmp.contractJson; + if (typeof contractJson !== 'object' || contractJson === null || !('storage' in contractJson)) { + return undefined; + } + const storage = contractJson.storage; + if (typeof storage !== 'object' || storage === null || !('storageHash' in storage)) + return undefined; + const hash = storage.storageHash; + return typeof hash === 'string' ? hash : undefined; +} From 8784ee42cace638114f9a748b12f943bdbc36515 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:03:24 +0200 Subject: [PATCH 03/12] test(app-cloud): add prisma-next test fixtures via a real CLI contract emit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two minimal, one-model contracts (Widget, Gadget), each with its TS-authored source (defineContract() + prisma-next.config.ts) and real emitted artifacts (contract.json + contract.d.ts) produced by running the installed @prisma-next/cli's `contract emit` offline — no live database needed. Source and emitted artifacts live in separate directories per fixture so a same-name .ts/.d.ts pair in one directory can't shadow each other during module resolution (this bit an earlier layout: an import of contract.d.ts silently resolved to the sibling contract.ts's inferred, non-branded type instead). Named widget-contract/gadget-contract rather than prisma-next(-other): a bare `prisma-next/` name is caught by the repo's .gitignore (an external-checkout ignore rule, not meant for this). Signed-off-by: willbot Signed-off-by: Will Madden --- .../gadget-contract/emitted/contract.d.ts | 178 ++++++++++++++++++ .../gadget-contract/emitted/contract.json | 133 +++++++++++++ .../gadget-contract/source/contract.ts | 15 ++ .../source/prisma-next.config.ts | 6 + .../widget-contract/emitted/contract.d.ts | 178 ++++++++++++++++++ .../widget-contract/emitted/contract.json | 133 +++++++++++++ .../widget-contract/source/contract.ts | 15 ++ .../source/prisma-next.config.ts | 6 + 8 files changed, 664 insertions(+) create mode 100644 packages/app-cloud/src/__tests__/fixtures/gadget-contract/emitted/contract.d.ts create mode 100644 packages/app-cloud/src/__tests__/fixtures/gadget-contract/emitted/contract.json create mode 100644 packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.ts create mode 100644 packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/prisma-next.config.ts create mode 100644 packages/app-cloud/src/__tests__/fixtures/widget-contract/emitted/contract.d.ts create mode 100644 packages/app-cloud/src/__tests__/fixtures/widget-contract/emitted/contract.json create mode 100644 packages/app-cloud/src/__tests__/fixtures/widget-contract/source/contract.ts create mode 100644 packages/app-cloud/src/__tests__/fixtures/widget-contract/source/prisma-next.config.ts diff --git a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/emitted/contract.d.ts b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/emitted/contract.d.ts new file mode 100644 index 000000000..0401f7dae --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/emitted/contract.d.ts @@ -0,0 +1,178 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:35caf6dcfd68f23c98de01af06ea7fe152b5be9a2258054b0156a5a98fe32037'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:ee7b22d50bbd2e3d2fb77f465c4151316b6a567d43ed63a30e7335cadd48d8df'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly public: { + readonly Gadget: { readonly id: Char<36>; readonly label: CodecTypes['pg/text@1']['output'] }; + }; +}; +export type FieldInputTypes = { + readonly public: { + readonly Gadget: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly label: CodecTypes['pg/text@1']['input']; + }; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType<{ + readonly namespaces: { + readonly public: { + readonly id: 'public'; + readonly kind: 'postgres-schema'; + readonly entries: { + readonly table: { + readonly Gadget: { + 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: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }>, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly Gadget: { readonly namespace: 'public' & NamespaceId; readonly model: 'Gadget' }; + }; + readonly domain: { + readonly namespaces: { + readonly public: { + readonly models: { + readonly Gadget: { + 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: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'Gadget'; + readonly namespaceId: 'public'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly label: { readonly column: 'label' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { + readonly namespace: 'public'; + readonly table: 'Gadget'; + 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']; diff --git a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/emitted/contract.json b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/emitted/contract.json new file mode 100644 index 000000000..d93cbaf2b --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/emitted/contract.json @@ -0,0 +1,133 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "Gadget": { + "model": "Gadget", + "namespace": "public" + } + }, + "domain": { + "namespaces": { + "public": { + "models": { + "Gadget": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "label": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "label": { + "column": "label" + } + }, + "namespaceId": "public", + "table": "Gadget" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "public": { + "entries": { + "table": { + "Gadget": { + "columns": { + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "label": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + }, + "id": "public", + "kind": "postgres-schema" + } + }, + "storageHash": "sha256:35caf6dcfd68f23c98de01af06ea7fe152b5be9a2258054b0156a5a98fe32037" + }, + "execution": { + "executionHash": "sha256:ee7b22d50bbd2e3d2fb77f465c4151316b6a567d43ed63a30e7335cadd48d8df", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "namespace": "public", + "table": "Gadget" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} \ No newline at end of file diff --git a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.ts b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.ts new file mode 100644 index 000000000..14042cbba --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.ts @@ -0,0 +1,15 @@ +import { defineContract } from '@prisma-next/postgres/contract-builder'; + +export const contract = defineContract( + { extensionPacks: {} }, + ({ field: f, model: m }) => ({ + models: { + Gadget: m('Gadget', { + fields: { + id: f.id.uuidv4String(), + label: f.text(), + }, + }), + }, + }), +); diff --git a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/prisma-next.config.ts b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/prisma-next.config.ts new file mode 100644 index 000000000..e1e2c59f9 --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/prisma-next.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '@prisma-next/postgres/config'; + +export default defineConfig({ + contract: './contract.ts', + db: { connection: 'postgres://localhost:5432/placeholder' }, +}); diff --git a/packages/app-cloud/src/__tests__/fixtures/widget-contract/emitted/contract.d.ts b/packages/app-cloud/src/__tests__/fixtures/widget-contract/emitted/contract.d.ts new file mode 100644 index 000000000..71a32e3f0 --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/widget-contract/emitted/contract.d.ts @@ -0,0 +1,178 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:f269d6251ab8c7c142448981686dc59d0c35f98c3971dcd0ed37585391bae668'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:c1073d26d2de37004dd7f1d65441c672074fcc86a2cb9c9079483a14ced78016'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly public: { + readonly Widget: { readonly id: Char<36>; readonly name: CodecTypes['pg/text@1']['output'] }; + }; +}; +export type FieldInputTypes = { + readonly public: { + readonly Widget: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly name: CodecTypes['pg/text@1']['input']; + }; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType<{ + readonly namespaces: { + readonly public: { + readonly id: 'public'; + readonly kind: 'postgres-schema'; + readonly entries: { + readonly table: { + readonly Widget: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }>, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly Widget: { readonly namespace: 'public' & NamespaceId; readonly model: 'Widget' }; + }; + readonly domain: { + readonly namespaces: { + readonly public: { + readonly models: { + readonly Widget: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'Widget'; + readonly namespaceId: 'public'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly name: { readonly column: 'name' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { + readonly namespace: 'public'; + readonly table: 'Widget'; + 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']; diff --git a/packages/app-cloud/src/__tests__/fixtures/widget-contract/emitted/contract.json b/packages/app-cloud/src/__tests__/fixtures/widget-contract/emitted/contract.json new file mode 100644 index 000000000..cc87320ca --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/widget-contract/emitted/contract.json @@ -0,0 +1,133 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "Widget": { + "model": "Widget", + "namespace": "public" + } + }, + "domain": { + "namespaces": { + "public": { + "models": { + "Widget": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "name": { + "column": "name" + } + }, + "namespaceId": "public", + "table": "Widget" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "public": { + "entries": { + "table": { + "Widget": { + "columns": { + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + }, + "id": "public", + "kind": "postgres-schema" + } + }, + "storageHash": "sha256:f269d6251ab8c7c142448981686dc59d0c35f98c3971dcd0ed37585391bae668" + }, + "execution": { + "executionHash": "sha256:c1073d26d2de37004dd7f1d65441c672074fcc86a2cb9c9079483a14ced78016", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "namespace": "public", + "table": "Widget" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} \ No newline at end of file diff --git a/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/contract.ts b/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/contract.ts new file mode 100644 index 000000000..f14040d1e --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/contract.ts @@ -0,0 +1,15 @@ +import { defineContract } from '@prisma-next/postgres/contract-builder'; + +export const contract = defineContract( + { extensionPacks: {} }, + ({ field: f, model: m }) => ({ + models: { + Widget: m('Widget', { + fields: { + id: f.id.uuidv4String(), + name: f.text(), + }, + }), + }, + }), +); diff --git a/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/prisma-next.config.ts b/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/prisma-next.config.ts new file mode 100644 index 000000000..e1e2c59f9 --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/prisma-next.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '@prisma-next/postgres/config'; + +export default defineConfig({ + contract: './contract.ts', + db: { connection: 'postgres://localhost:5432/placeholder' }, +}); From 483b1e179f1855e7ef0c95b4df7d09ae730d2f96 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:03:30 +0200 Subject: [PATCH 04/12] test(app-cloud): prove pnPostgres's shapes and storageHash-exact wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type test (vitest --typecheck): a consumer requiring the emitted Widget contract is assignable where the resource provides that same contract type (including a second, independently-built wrap of it — the lever is the type, not object identity); a different emitted contract (Gadget, a different storageHash) is a type error, and so is a resource of a wholly different protocol kind (bare postgresContract). Unit test (bun test): satisfies() is true for equal storageHash and false otherwise (including across kinds); the factory returns correctly branded ResourceNode/DependencyEnd shapes; hydrate constructs a real Prisma Next client from a fake url without connecting, and never throws regardless of what a live marker would say. Signed-off-by: willbot Signed-off-by: Will Madden --- .../__tests__/prisma-next-shapes.test-d.ts | 95 +++++++++++++++ .../src/__tests__/prisma-next.test.ts | 111 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 packages/app-cloud/src/__tests__/prisma-next-shapes.test-d.ts create mode 100644 packages/app-cloud/src/__tests__/prisma-next.test.ts diff --git a/packages/app-cloud/src/__tests__/prisma-next-shapes.test-d.ts b/packages/app-cloud/src/__tests__/prisma-next-shapes.test-d.ts new file mode 100644 index 000000000..0d7b9317a --- /dev/null +++ b/packages/app-cloud/src/__tests__/prisma-next-shapes.test-d.ts @@ -0,0 +1,95 @@ +/** + * `pnPostgres()`'s two shapes, and storageHash-exact wiring compatibility. + * `{ name, config }` is the provisionable identity; `pnPostgres(contract)` is + * the dependency, whose binding is the typed Prisma Next client. The + * `WidgetContract`/`GadgetContract` types come from real `prisma-next + * contract emit` output (`fixtures/{widget,gadget}-contract/emitted/contract.d.ts`) + * — the branded `storageHash` literal each carries is the lever under test. + * + * Type-only (vitest `--typecheck`, never executed). Positive cases assert + * the returned role and the binding via `expectTypeOf`; the wiring reject + * case keeps a `// @ts-expect-error`. + */ +import type { + BuildAdapter, + DependencyEnd, + Hydrated, + ResourceNode, + SystemBuilder, +} from '@prisma/app'; +import { service } from '@prisma/app'; +import { expectTypeOf, test } from 'vitest'; +import { postgres } from '../postgres.ts'; +import { type Client, type PnPostgresContract, pnContract, pnPostgres } from '../prisma-next.ts'; +import type { Contract as WidgetContract } from './fixtures/widget-contract/emitted/contract.d.ts'; +import type { Contract as GadgetContract } from './fixtures/gadget-contract/emitted/contract.d.ts'; + +declare const widgetJson: WidgetContract; +declare const widgetJsonAgain: WidgetContract; +declare const gadgetJson: GadgetContract; + +const widget = pnContract(widgetJson); +// A second, independently-wrapped value carrying the SAME emitted contract +// type — proves the lever is the type (storageHash), not object identity. +const widgetAgain = pnContract(widgetJsonAgain); +const gadget = pnContract(gadgetJson); + +test('pnContract wraps an emitted contract into the prisma-next kind', () => { + expectTypeOf(widget).toEqualTypeOf>(); +}); + +test('{ name, config } yields the resource identity providing config.contract', () => { + const identity = pnPostgres({ name: 'db', config: { contract: widget } }); + expectTypeOf(identity).toEqualTypeOf>(); +}); + +test('pnPostgres(contract) yields the dependency requiring that contract; its binding is the typed client', () => { + const dep = pnPostgres(widget); + expectTypeOf(dep).toEqualTypeOf, typeof widget>>(); + // The binding load() hands the app is the Prisma Next client typed by the contract. + expectTypeOf>().toEqualTypeOf>(); +}); + +const build: BuildAdapter = { + extension: '@prisma/app-node', + type: 'node', + module: 'file:///test/service.ts', + entry: 'server.js', +}; + +const consumer = service({ + name: 'test-service', + extension: 'test/pack', + type: 'fake/compute', + inputs: { db: pnPostgres(widget) }, + params: {}, + build, +}); + +declare const h: SystemBuilder; + +const widgetRef = h.provision('db1', pnPostgres({ name: 'db', config: { contract: widget } })); +const widgetAgainRef = h.provision( + 'db2', + pnPostgres({ name: 'db', config: { contract: widgetAgain } }), +); +const gadgetRef = h.provision('db3', pnPostgres({ name: 'db', config: { contract: gadget } })); + +test('a resource providing the SAME emitted contract (same storageHash) satisfies the dependency slot', () => { + expectTypeOf(h.provision).toBeCallableWith('c1', consumer, { db: widgetRef }); + // A different wrap of the identical emitted contract type also satisfies — + // the lever is the type (storageHash), not which `pnContract()` call built it. + expectTypeOf(h.provision).toBeCallableWith('c2', consumer, { db: widgetAgainRef }); +}); + +test('a resource providing a DIFFERENT emitted contract (different storageHash) is a type error', () => { + // @ts-expect-error different storageHash — not assignable to the widget-requiring slot + h.provision('c3', consumer, { db: gadgetRef }); +}); + +const barePostgresRef = h.provision('db4', postgres({ name: 'db4' })); + +test('a resource of a different protocol kind entirely (bare postgresContract) is a type error', () => { + // @ts-expect-error postgresContract's kind is "postgres", not "prisma-next" + h.provision('c4', consumer, { db: barePostgresRef }); +}); diff --git a/packages/app-cloud/src/__tests__/prisma-next.test.ts b/packages/app-cloud/src/__tests__/prisma-next.test.ts new file mode 100644 index 000000000..66b476752 --- /dev/null +++ b/packages/app-cloud/src/__tests__/prisma-next.test.ts @@ -0,0 +1,111 @@ +/** + * `pnPostgres()`'s runtime behavior — proven without a live database. The PN + * client is lazy (its pool opens on first query), so hydrate is fully + * exercisable here: `pnPostgresRuntime()` never connects just by being + * constructed (see `fixtures/widget-contract/`'s round trip through the real + * `prisma-next contract emit` CLI for the artifacts this suite imports). + * + * `pnContract(contractJson)` pins the type parameter explicitly — + * a JSON module import's inferred type is plain data, not the branded + * `contract.d.ts` type, matching `@prisma-next/postgres/runtime`'s own + * `postgres({ contractJson })` convention (see prisma-next.ts). + */ +import { describe, expect, test } from 'bun:test'; +import { isNode } from '@prisma/app'; +import { pnContract, pnPostgres } from '../prisma-next.ts'; +import type { Contract as WidgetContract } from './fixtures/widget-contract/emitted/contract.d.ts'; +import widgetContractJson from './fixtures/widget-contract/emitted/contract.json' with { type: 'json' }; +import type { Contract as GadgetContract } from './fixtures/gadget-contract/emitted/contract.d.ts'; +import gadgetContractJson from './fixtures/gadget-contract/emitted/contract.json' with { + type: 'json', +}; + +describe('pnContract().satisfies()', () => { + test('true when the required contract has the same storageHash', () => { + const a = pnContract(widgetContractJson); + const b = pnContract(widgetContractJson); + expect(a.satisfies(b)).toBe(true); + expect(b.satisfies(a)).toBe(true); + }); + + test('false when the required contract has a different storageHash', () => { + const widget = pnContract(widgetContractJson); + const gadget = pnContract(gadgetContractJson); + expect(widget.satisfies(gadget)).toBe(false); + expect(gadget.satisfies(widget)).toBe(false); + }); + + test('the wrapped contract is frozen and carries the prisma-next kind', () => { + const widget = pnContract(widgetContractJson); + expect(widget.kind).toBe('prisma-next'); + expect(Object.isFrozen(widget)).toBe(true); + }); +}); + +describe('pnPostgres() factory shapes', () => { + test('{ name, config } yields a branded ResourceNode providing config.contract', () => { + const widget = pnContract(widgetContractJson); + const node = pnPostgres({ name: 'database', config: { contract: widget } }); + + expect(isNode(node)).toBe(true); + expect(node.kind).toBe('resource'); + expect(node.name).toBe('database'); + expect(node.extension).toBe('@prisma/app-cloud'); + expect(node.type).toBe('prisma-next'); + expect(node.provides).toBe(widget); + }); + + test('config.connection is accepted and ignored — the framework injects the URL at hydrate', () => { + const widget = pnContract(widgetContractJson); + const node = pnPostgres({ + name: 'database', + config: { contract: widget, connection: 'postgres://ignored' }, + }); + expect(node.provides).toBe(widget); + }); + + test('pnPostgres(contract) yields a branded DependencyEnd requiring that contract', () => { + const widget = pnContract(widgetContractJson); + const dep = pnPostgres(widget); + + expect(isNode(dep)).toBe(true); + expect(dep.kind).toBe('dependency'); + expect(dep.type).toBe('prisma-next'); + expect(dep.required).toBe(widget); + expect(Object.keys(dep.connection.params)).toEqual(['url']); + expect(dep.connection.params['url']).toEqual({ type: 'string', secret: true }); + }); +}); + +describe('hydrate — no live database required (lazy pool)', () => { + test('constructs a Prisma Next client from a fake url without connecting', async () => { + const widget = pnContract(widgetContractJson); + const dep = pnPostgres(widget); + + const client = await dep.connection.hydrate({ + url: 'postgres://user:pass@localhost:5432/does-not-exist', + }); + + // The PostgresClient surface — constructed synchronously; nothing here + // implies a connection was opened (pool.connect() only happens on first + // query/`.runtime()`/`.connect()` call, none of which this test makes). + expect(typeof client.sql).toBe('object'); + expect(typeof client.orm).toBe('object'); + expect(typeof client.connect).toBe('function'); + expect(typeof client.runtime).toBe('function'); + expect(typeof client.close).toBe('function'); + }); + + test('a mismatched-marker verifyMarker setting never throws at construct time', () => { + // verifyMarker only runs lazily on first query (see module doc comment); + // this asserts hydrate itself — which sets `verifyMarker: 'onFirstUse'` + // — never throws just by constructing the client, regardless of what a + // live database's marker would say. + const widget = pnContract(widgetContractJson); + const dep = pnPostgres(widget); + + expect(() => + dep.connection.hydrate({ url: 'postgres://user:pass@localhost:5432/mismatched' }), + ).not.toThrow(); + }); +}); From 8e5cea4450d98d9972a9c338121dea3dbc0cbd4e Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:03:35 +0200 Subject: [PATCH 05/12] test(app-cloud): extend invariants for the new ./prisma-next entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the entry-map invariant to expect the new subpath export, and adds invariant 7: no module reachable from src/index.ts imports prisma-next.ts, @prisma-next/*, or pg, and the built dist/index.mjs contains none of those tokens — the DoD's "index import stays free of @prisma-next/*/pg" checked directly against the built output, not just reasoned about. Signed-off-by: willbot Signed-off-by: Will Madden --- .../src/__tests__/invariants.test.ts | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/app-cloud/src/__tests__/invariants.test.ts b/packages/app-cloud/src/__tests__/invariants.test.ts index fbcb276fd..2d7fc654d 100644 --- a/packages/app-cloud/src/__tests__/invariants.test.ts +++ b/packages/app-cloud/src/__tests__/invariants.test.ts @@ -22,12 +22,12 @@ function shippedSources(): { file: string; text: string }[] { return out; } -describe('entry map: the extension splits into authoring + control only', () => { - test("package.json exports exactly '.' and './control' — no runtime entry", () => { +describe('entry map: the extension splits into authoring + control + prisma-next only', () => { + test("package.json exports exactly '.', './control', and './prisma-next' — no other runtime entry", () => { const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8')); // `./package.json` is a conventional manifest export, not a code entry. const codeEntries = Object.keys(pkg.exports).filter((k) => k !== './package.json'); - expect(codeEntries.sort()).toEqual(['.', './control']); + expect(codeEntries.sort()).toEqual(['.', './control', './prisma-next']); }); }); @@ -160,3 +160,55 @@ describe('invariant 6 (ADR-0017, extension config): the authoring entry never re } }); }); + +describe('invariant 7 (ADR-0021): the authoring entry never reaches the prisma-next entry', () => { + test('no module reachable from src/index.ts imports the /prisma-next entry — Prisma Next stays opt-in', () => { + // prisma-next.ts (and transitively @prisma-next/postgres + pg) is + // imported only by an app that explicitly imports the ./prisma-next + // subpath. A reachable import from the authoring barrel would drag that + // dependency tree into every service, defeating the whole point of the + // dedicated subpath entry (ADR-0021, design-notes.md "opt-out stays real"). + const importPattern = + /(?:import|export)\s+[^'"]*?from\s*["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|import\s*["']([^"']+)["']/g; + const importSpecifiers = (text: string): string[] => { + const out: string[] = []; + for (const match of text.matchAll(importPattern)) { + const spec = match[1] ?? match[2] ?? match[3]; + if (spec !== undefined) out.push(spec); + } + return out; + }; + + const seen = new Map(); + const queue = [path.join(srcDir, 'index.ts')]; + while (queue.length > 0) { + const file = queue.pop(); + if (file === undefined || seen.has(file)) continue; + const specs = importSpecifiers(fs.readFileSync(file, 'utf8')); + seen.set(file, specs); + for (const spec of specs) { + if (!spec.startsWith('.')) continue; + const resolved = path.resolve(path.dirname(file), spec); + if (fs.existsSync(resolved)) queue.push(resolved); + } + } + + expect(seen.size).toBeGreaterThan(0); + for (const [file, specs] of seen) { + const offending = specs.filter( + (spec) => + /\/prisma-next(\.ts)?$/.test(spec) || spec.startsWith('@prisma-next/') || spec === 'pg', + ); + expect({ file: path.relative(srcDir, file), offending }).toEqual({ + file: path.relative(srcDir, file), + offending: [], + }); + } + }); + + test('the built dist/index.mjs contains no @prisma-next/* or pg tokens', () => { + const built = fs.readFileSync(path.join(pkgDir, 'dist', 'index.mjs'), 'utf8'); + expect(built).not.toContain('@prisma-next/'); + expect(built.includes('"pg"') || built.includes("'pg'")).toBe(false); + }); +}); From d9ea42b232156cb128b252b7efe6a4ff57303a2d Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:19:03 +0200 Subject: [PATCH 06/12] refactor(app-cloud)!: pnPostgres resource takes { name, contract }; drop runtime marker check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two locked design changes (ADR-0021, rewritten): - The resource overload is now `pnPostgres({ name, contract })` — the consumed emitted contract, nothing else. Deletes `PnPostgresConfig` and its `connection?` field: the `prisma-next.config.ts` path the deploy migration step needs is slice-2 metadata that rides on the resource there, not a field the app build declares (the frozen ResourceNode has no metadata slot today; that mechanism is slice 2's call). - Hydrate builds the client with no `verifyMarker`. Schema correctness is a build/deploy-time job: the deploy migrates the DB to the contract hash and keeps it there, so the runtime binding just builds the client. A running service can't be crashed by a marker check because there is none. The dependency end (`pnPostgres(contract)`) and the `satisfies` storageHash comparison are unchanged. Signed-off-by: willbot Signed-off-by: Will Madden --- packages/app-cloud/src/prisma-next.ts | 61 +++++++++------------------ 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/packages/app-cloud/src/prisma-next.ts b/packages/app-cloud/src/prisma-next.ts index d9720bac8..bfac2f13a 100644 --- a/packages/app-cloud/src/prisma-next.ts +++ b/packages/app-cloud/src/prisma-next.ts @@ -1,11 +1,11 @@ /** * `pnPostgres()` is the `prisma-next` kind's single entry, overloaded by what * it's given — the typed sibling of bare `postgres()` (untouched in - * `postgres.ts`). A resource end takes `{ name, config }`, where - * `config.contract` anchors the resource's provided contract; a dependency - * end takes that same wrapped contract value and hydrates to a typed Prisma - * Next client, `Client`, over the injected `{ url }` connection. - * This amends ADR-0015 for this one dep kind (see ADR-0021): Prisma Next is + * `postgres.ts`). A resource end takes `{ name, contract }`, where `contract` + * is the consumed emitted artifact the resource provides; a dependency end + * takes that same wrapped contract value and hydrates to a typed Prisma Next + * client, `Client`, over the injected `{ url }` connection. This + * amends ADR-0015 for this one dep kind (see ADR-0021): Prisma Next is * framework-blessed like rpc, so the binding is the typed client itself, not * a config the app builds its own client from. * @@ -18,18 +18,11 @@ * version-equality test, since two distinct in-memory contract values can * carry the same hash). * - * Marker verification is warn-only by construction, not by a wrapper here: - * Prisma Next's own `verifyMarker: 'onFirstUse'` never throws on a mismatch - * — confirmed by reading `@prisma-next/sql-runtime`'s `verifyMarker()` - * implementation (not just its `.d.ts`): a missing or mismatched marker logs - * `CONTRACT.MARKER_MISSING` / `CONTRACT.MARKER_MISMATCH` and returns. The one - * gap: `@prisma-next/postgres@0.14.0`'s public `postgres()` options don't - * forward a `log` sink to the runtime, so that warning lands in Prisma - * Next's internal no-op logger and isn't visible anywhere by default in this - * version — a Prisma Next limitation, not something hydrate can route around - * without depending on unexported internals or querying the DB at hydrate - * (which would break the lazy-pool contract). The "never throws" half of the - * requirement holds regardless. + * There is NO runtime schema verification. Schema correctness is a + * build/deploy-time job: the deploy migrates the database to the contract's + * hash and guarantees it stays there, so hydrate just builds the client (no + * `verifyMarker`). A running service can't be crashed by a marker check + * because there is no marker check (ADR-0021). */ import type { Contract, DependencyEnd, ResourceNode } from '@prisma/app'; @@ -72,26 +65,10 @@ export type PnContractOf = Ct extends PnPostgresContract ? C : neve /** The typed client a consumer's `pnPostgres(contract)` dependency hydrates to. */ export type Client = PostgresClient>; -/** - * The resource end's config — the framework's own thin wrapper carrying the - * wrapped `prisma-next` contract this database provides. Deliberately NOT - * `@prisma-next/postgres/config`'s own `PrismaNextConfig`: that type anchors - * Prisma Next's OWN CLI (`contract emit` / `migrate`) to a - * `prisma-next.config.ts` file on disk, which is a separate, parallel - * artifact this dispatch does not read (the deploy lowering that will read - * it is slice 2's job). `connection` is accepted only to mirror the shape - * the design sketched and is always ignored — the framework injects the - * connection URL at hydrate (no-globals). - */ -export interface PnPostgresConfig { - readonly contract: C; - readonly connection?: unknown; -} - /** * Wraps a resolved Prisma Next contract value into the framework's - * `prisma-next` Contract kind. Both the resource end's `config.contract` and - * every dependency end reference the SAME wrapped value, the same way + * `prisma-next` Contract kind. Both the resource end's `contract` and every + * dependency end reference the SAME wrapped value, the same way * `@prisma/app-rpc`'s `contract()` output feeds both a service's `expose` * and its consumers' `rpc(contract)` deps. * @@ -119,12 +96,15 @@ export function pnContract(contract: unknown): unknown { } /** - * `{ name, config }` — the resource identity a system provisions: the ONE - * place the database exists, providing `config.contract`. + * `{ name, contract }` — the resource identity a system provisions: the ONE + * place the database exists, providing `contract`. The `prisma-next.config.ts` + * path the deploy migration step needs is NOT declared here — it rides on the + * resource in slice 2, alongside the lowering that reads it (ADR-0021); the + * app build never imports the config. */ export function pnPostgres(opts: { name: string; - config: PnPostgresConfig; + contract: C; }): ResourceNode; /** * `pnPostgres(contract)` — a service's dependency on a Prisma Next-typed @@ -133,13 +113,13 @@ export function pnPostgres(opts: { */ export function pnPostgres(contract: C): DependencyEnd, C>; export function pnPostgres( - arg: { name: string; config: PnPostgresConfig } | PnPostgresContract, + arg: { name: string; contract: PnPostgresContract } | PnPostgresContract, ): unknown { if (!isPnPostgresContract(arg)) { return resource({ name: arg.name, extension: '@prisma/app-cloud', - provides: arg.config.contract, + provides: arg.contract, }); } const contract = arg; @@ -168,7 +148,6 @@ function buildClient(contract: C, url: string): Cl return pnPostgresRuntime>({ contractJson: contract.__cmp.contractJson, url, - verifyMarker: 'onFirstUse', }); } From 1f6c1b57b51dd923213e20288520eeb943c58cbe Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:19:10 +0200 Subject: [PATCH 07/12] test(app-cloud): match the { name, contract } resource; cover missing-hash satisfies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Type + unit tests construct the resource via `pnPostgres({ name, contract })`; the removed `config.connection`-is-ignored case goes with the removed field. - The hydrate test now asserts the design directly: hydrate does no schema verification and reads no database, so it can't throw regardless of any live marker (there is no `verifyMarker`). - Adds the reviewer's missing-hash coverage for `satisfies`: a required contract with a malformed `__cmp` (no `contractJson`) is not satisfied, and a wrapper whose `contractJson` lacks `storage.storageHash` is unsatisfiable in both directions (and does not even satisfy itself) — exercising the `storageHashOf` undefined branches that were correct but untested. Signed-off-by: willbot Signed-off-by: Will Madden --- .../__tests__/prisma-next-shapes.test-d.ts | 17 +++--- .../src/__tests__/prisma-next.test.ts | 56 ++++++++++++------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/app-cloud/src/__tests__/prisma-next-shapes.test-d.ts b/packages/app-cloud/src/__tests__/prisma-next-shapes.test-d.ts index 0d7b9317a..faaf06fcd 100644 --- a/packages/app-cloud/src/__tests__/prisma-next-shapes.test-d.ts +++ b/packages/app-cloud/src/__tests__/prisma-next-shapes.test-d.ts @@ -1,6 +1,6 @@ /** * `pnPostgres()`'s two shapes, and storageHash-exact wiring compatibility. - * `{ name, config }` is the provisionable identity; `pnPostgres(contract)` is + * `{ name, contract }` is the provisionable identity; `pnPostgres(contract)` is * the dependency, whose binding is the typed Prisma Next client. The * `WidgetContract`/`GadgetContract` types come from real `prisma-next * contract emit` output (`fixtures/{widget,gadget}-contract/emitted/contract.d.ts`) @@ -21,8 +21,8 @@ import { service } from '@prisma/app'; import { expectTypeOf, test } from 'vitest'; import { postgres } from '../postgres.ts'; import { type Client, type PnPostgresContract, pnContract, pnPostgres } from '../prisma-next.ts'; -import type { Contract as WidgetContract } from './fixtures/widget-contract/emitted/contract.d.ts'; import type { Contract as GadgetContract } from './fixtures/gadget-contract/emitted/contract.d.ts'; +import type { Contract as WidgetContract } from './fixtures/widget-contract/emitted/contract.d.ts'; declare const widgetJson: WidgetContract; declare const widgetJsonAgain: WidgetContract; @@ -38,8 +38,8 @@ test('pnContract wraps an emitted contract into the prisma-next kind', () => { expectTypeOf(widget).toEqualTypeOf>(); }); -test('{ name, config } yields the resource identity providing config.contract', () => { - const identity = pnPostgres({ name: 'db', config: { contract: widget } }); +test('{ name, contract } yields the resource identity providing contract', () => { + const identity = pnPostgres({ name: 'db', contract: widget }); expectTypeOf(identity).toEqualTypeOf>(); }); @@ -68,12 +68,9 @@ const consumer = service({ declare const h: SystemBuilder; -const widgetRef = h.provision('db1', pnPostgres({ name: 'db', config: { contract: widget } })); -const widgetAgainRef = h.provision( - 'db2', - pnPostgres({ name: 'db', config: { contract: widgetAgain } }), -); -const gadgetRef = h.provision('db3', pnPostgres({ name: 'db', config: { contract: gadget } })); +const widgetRef = h.provision('db1', pnPostgres({ name: 'db', contract: widget })); +const widgetAgainRef = h.provision('db2', pnPostgres({ name: 'db', contract: widgetAgain })); +const gadgetRef = h.provision('db3', pnPostgres({ name: 'db', contract: gadget })); test('a resource providing the SAME emitted contract (same storageHash) satisfies the dependency slot', () => { expectTypeOf(h.provision).toBeCallableWith('c1', consumer, { db: widgetRef }); diff --git a/packages/app-cloud/src/__tests__/prisma-next.test.ts b/packages/app-cloud/src/__tests__/prisma-next.test.ts index 66b476752..5f046dcef 100644 --- a/packages/app-cloud/src/__tests__/prisma-next.test.ts +++ b/packages/app-cloud/src/__tests__/prisma-next.test.ts @@ -11,14 +11,17 @@ * `postgres({ contractJson })` convention (see prisma-next.ts). */ import { describe, expect, test } from 'bun:test'; +import type { Contract } from '@prisma/app'; import { isNode } from '@prisma/app'; import { pnContract, pnPostgres } from '../prisma-next.ts'; -import type { Contract as WidgetContract } from './fixtures/widget-contract/emitted/contract.d.ts'; -import widgetContractJson from './fixtures/widget-contract/emitted/contract.json' with { type: 'json' }; import type { Contract as GadgetContract } from './fixtures/gadget-contract/emitted/contract.d.ts'; import gadgetContractJson from './fixtures/gadget-contract/emitted/contract.json' with { type: 'json', }; +import type { Contract as WidgetContract } from './fixtures/widget-contract/emitted/contract.d.ts'; +import widgetContractJson from './fixtures/widget-contract/emitted/contract.json' with { + type: 'json', +}; describe('pnContract().satisfies()', () => { test('true when the required contract has the same storageHash', () => { @@ -35,6 +38,31 @@ describe('pnContract().satisfies()', () => { expect(gadget.satisfies(widget)).toBe(false); }); + test('false when the required contract carries a malformed __cmp (no contractJson)', () => { + const widget = pnContract(widgetContractJson); + // A prisma-next-kinded value whose __cmp lacks contractJson entirely — + // storageHashOf() returns undefined, so satisfies() must be false rather + // than throw or spuriously match. + const malformed = { + kind: 'prisma-next', + __cmp: {}, + satisfies: () => false, + } as Contract<'prisma-next', unknown>; + expect(widget.satisfies(malformed)).toBe(false); + }); + + test("false in both directions when a wrapper's contractJson lacks storage.storageHash", () => { + const widget = pnContract(widgetContractJson); + // contractJson is present but shaped wrong: no `storage.storageHash`. + // Both this wrapper's own hash and any comparison against it resolve to + // undefined, so satisfies() is false whichever side asks. + const hashless = pnContract({ storage: { namespaces: {} } }); + expect(widget.satisfies(hashless)).toBe(false); + expect(hashless.satisfies(widget)).toBe(false); + // ...and a hashless wrapper does not even satisfy itself. + expect(hashless.satisfies(hashless)).toBe(false); + }); + test('the wrapped contract is frozen and carries the prisma-next kind', () => { const widget = pnContract(widgetContractJson); expect(widget.kind).toBe('prisma-next'); @@ -43,9 +71,9 @@ describe('pnContract().satisfies()', () => { }); describe('pnPostgres() factory shapes', () => { - test('{ name, config } yields a branded ResourceNode providing config.contract', () => { + test('{ name, contract } yields a branded ResourceNode providing contract', () => { const widget = pnContract(widgetContractJson); - const node = pnPostgres({ name: 'database', config: { contract: widget } }); + const node = pnPostgres({ name: 'database', contract: widget }); expect(isNode(node)).toBe(true); expect(node.kind).toBe('resource'); @@ -55,15 +83,6 @@ describe('pnPostgres() factory shapes', () => { expect(node.provides).toBe(widget); }); - test('config.connection is accepted and ignored — the framework injects the URL at hydrate', () => { - const widget = pnContract(widgetContractJson); - const node = pnPostgres({ - name: 'database', - config: { contract: widget, connection: 'postgres://ignored' }, - }); - expect(node.provides).toBe(widget); - }); - test('pnPostgres(contract) yields a branded DependencyEnd requiring that contract', () => { const widget = pnContract(widgetContractJson); const dep = pnPostgres(widget); @@ -96,16 +115,15 @@ describe('hydrate — no live database required (lazy pool)', () => { expect(typeof client.close).toBe('function'); }); - test('a mismatched-marker verifyMarker setting never throws at construct time', () => { - // verifyMarker only runs lazily on first query (see module doc comment); - // this asserts hydrate itself — which sets `verifyMarker: 'onFirstUse'` - // — never throws just by constructing the client, regardless of what a - // live database's marker would say. + test('hydrate does no schema verification — it just builds the client', () => { + // There is no runtime marker check (ADR-0021): schema correctness is a + // deploy-time job. Constructing the client can't be crashed by a marker + // mismatch because hydrate sets no `verifyMarker` and reads no database. const widget = pnContract(widgetContractJson); const dep = pnPostgres(widget); expect(() => - dep.connection.hydrate({ url: 'postgres://user:pass@localhost:5432/mismatched' }), + dep.connection.hydrate({ url: 'postgres://user:pass@localhost:5432/any-db' }), ).not.toThrow(); }); }); From 8f034747da28a376ebaaa15563275704d0aabd0f Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:29:23 +0200 Subject: [PATCH 08/12] test(app-cloud): add a local Postgres harness for the integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors packages/alchemy/src/state/__tests__/harness.ts: honors STATE_TEST_DATABASE_URL (the env var the CI test job already wires), else spins an ephemeral initdb/pg_ctl cluster, else returns undefined so the caller skips loudly on a dev machine without Postgres — and throws on CI so the suite can never silently go unexecuted. Kept as a local copy rather than a cross-package import: that harness is another package's private test file with no export, and the workspace keeps a package's tests self-contained. Signed-off-by: willbot Signed-off-by: Will Madden --- .../src/__tests__/postgres-harness.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 packages/app-cloud/src/__tests__/postgres-harness.ts diff --git a/packages/app-cloud/src/__tests__/postgres-harness.ts b/packages/app-cloud/src/__tests__/postgres-harness.ts new file mode 100644 index 000000000..4559c1f90 --- /dev/null +++ b/packages/app-cloud/src/__tests__/postgres-harness.ts @@ -0,0 +1,127 @@ +/** + * A throwaway local Postgres for the prisma-next integration test — mirrors + * `packages/alchemy/src/state/__tests__/harness.ts` (the state store's harness) + * so both suites share one Postgres availability contract and one CI service + * container. Kept as a local copy rather than a cross-package import: that + * harness is another package's private test file with no export, and the + * workspace convention keeps a package's tests self-contained. + * + * Availability signaling is identical: honors `STATE_TEST_DATABASE_URL` (the + * env var the CI test job already wires), else spins an ephemeral cluster via + * `initdb`/`pg_ctl`, else returns `undefined` locally (caller skips loudly) — + * except on CI, where the absence of both throws so the suite can't silently + * go unexecuted. + */ +import { execFileSync, spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +export interface TestPostgres { + readonly url: string; + readonly stop: () => void; +} + +// Some sandboxes leave LANG/LC_* unset or pointed at a locale glibc/ICU can't +// resolve, which makes postmaster become multithreaded during startup and +// refuse to serve. Pin C for every initdb/pg_ctl invocation. +const PG_ENV = { ...process.env, LC_ALL: 'C', LANG: 'C' }; + +const probe = (bin: string): boolean => + spawnSync(bin, ['--version'], { stdio: 'ignore', env: PG_ENV }).status === 0; + +// Ubuntu's postgresql-common packaging installs versioned server binaries +// under /usr/lib/postgresql//bin, never on PATH — glob every version. +const globUbuntuPostgresqlBinCandidates = (name: string): string[] => { + const base = '/usr/lib/postgresql'; + try { + return fs.readdirSync(base).map((version) => path.join(base, version, 'bin', name)); + } catch { + return []; + } +}; + +const findBinary = (name: string): string | undefined => { + const candidates = [ + name, + `/opt/homebrew/opt/postgresql@15/bin/${name}`, + `/opt/homebrew/bin/${name}`, + `/usr/local/opt/postgresql@15/bin/${name}`, + `/usr/local/bin/${name}`, + ...globUbuntuPostgresqlBinCandidates(name), + ]; + return candidates.find(probe); +}; + +/** + * Synchronously starts (or reuses) a throwaway Postgres. Runs at module load + * — before `describe.skipIf` gates the suite — because bun collects tests + * synchronously, so availability must be known before the file registers its + * `describe` blocks. + * + * Returns `undefined` when neither `STATE_TEST_DATABASE_URL` nor initdb/pg_ctl + * is available and `CI` is unset (a dev machine without Postgres) — callers + * skip loudly. On CI the absence of both throws instead, so the suite can + * never quietly go unexecuted. + */ +export const startTestPostgres = (): TestPostgres | undefined => { + const fromEnv = process.env['STATE_TEST_DATABASE_URL']; + if (fromEnv !== undefined) { + return { url: fromEnv, stop: () => {} }; + } + + const initdb = findBinary('initdb'); + const pgCtl = findBinary('pg_ctl'); + if (initdb === undefined || pgCtl === undefined) { + if (process.env['CI'] !== undefined) { + throw new Error( + 'CI is set but no Postgres is available for the prisma-next integration test: neither ' + + 'STATE_TEST_DATABASE_URL nor initdb/pg_ctl (PATH, Homebrew, or Ubuntu ' + + '/usr/lib/postgresql/*/bin) were found. The CI test job wires a `services: postgres:` ' + + 'container and STATE_TEST_DATABASE_URL (see .github/workflows/ci.yml).', + ); + } + return undefined; + } + + const baseDir = process.env['STATE_TEST_PG_TMPDIR'] ?? os.tmpdir(); + fs.mkdirSync(baseDir, { recursive: true }); + const dataDir = fs.mkdtempSync(path.join(baseDir, 'prisma-app-pn-pg-')); + const logFile = path.join(dataDir, 'server.log'); + + execFileSync( + initdb, + ['-D', dataDir, '-U', 'postgres', '--auth=trust', '-E', 'UTF8', '--locale=C'], + { stdio: 'pipe', env: PG_ENV }, + ); + + let lastError = 'unknown error'; + for (let attempt = 0; attempt < 5; attempt++) { + const port = 20000 + Math.floor(Math.random() * 20000); + const result = spawnSync( + pgCtl, + ['-D', dataDir, '-o', `-p ${port} -h 127.0.0.1`, '-w', '-l', logFile, 'start'], + { stdio: 'pipe', env: PG_ENV }, + ); + if (result.status === 0) { + return { + url: `postgres://postgres@127.0.0.1:${port}/postgres`, + stop: () => { + try { + execFileSync(pgCtl, ['-D', dataDir, '-m', 'fast', 'stop'], { + stdio: 'pipe', + env: PG_ENV, + }); + } finally { + fs.rmSync(dataDir, { recursive: true, force: true }); + } + }, + }; + } + lastError = result.stderr.toString(); + } + fs.rmSync(dataDir, { recursive: true, force: true }); + throw new Error( + `initdb/pg_ctl were found on PATH but the ephemeral test Postgres failed to start after 5 attempts: ${lastError}`, + ); +}; From 829aa8082462f72d5a239f710a8d5147b39f4fc7 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:29:33 +0200 Subject: [PATCH 09/12] test(app-cloud): prove the hydrated pnPostgres client live against real Postgres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The slice's live proof (D2): stand up a real Postgres, bring its schema to the widget-contract fixture via Prisma Next's own control client (createPostgresControlClient().dbInit({ mode: 'apply' }) — the machinery slice 2's deploy lowering will drive, so this exercises the real apply-and-sign path, not hand-written SQL), construct the client exactly as a service would through pnPostgres(contract)'s hydrate given only the DB url, and round-trip a real query: ORM create() then where().first()/all(), asserting the row. The result is contract-typed end to end — Widget.id is a char(36) column, so its typed value is the branded Char<36>; a plain string does not compile as the id, which is what makes the round-trip's type-check the typed-result proof (the typecheck gate enforces it). Adds @prisma-next/target-postgres as a devDep for that Char type (a direct import in the test). Environment-gated via the harness: skips cleanly with no Postgres locally, runs on CI against the wired postgres service. Verified live here against a spawned ephemeral cluster. Signed-off-by: willbot Signed-off-by: Will Madden --- packages/app-cloud/package.json | 1 + .../__tests__/prisma-next.integration.test.ts | 100 ++++++++++++++++++ pnpm-lock.yaml | 3 + 3 files changed, 104 insertions(+) create mode 100644 packages/app-cloud/src/__tests__/prisma-next.integration.test.ts diff --git a/packages/app-cloud/package.json b/packages/app-cloud/package.json index 35ca4c935..381edd580 100644 --- a/packages/app-cloud/package.json +++ b/packages/app-cloud/package.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@prisma-next/cli": "0.14.0", + "@prisma-next/target-postgres": "0.14.0", "@prisma/app-tsdown": "workspace:0.2.0", "@types/bun": "^1.3.13", "tsdown": "^0.15.9", diff --git a/packages/app-cloud/src/__tests__/prisma-next.integration.test.ts b/packages/app-cloud/src/__tests__/prisma-next.integration.test.ts new file mode 100644 index 000000000..d3e3447e0 --- /dev/null +++ b/packages/app-cloud/src/__tests__/prisma-next.integration.test.ts @@ -0,0 +1,100 @@ +/** + * The live proof: a hydrated Prisma Next client round-trips a real query + * against a real Postgres whose schema is at the `widget-contract` fixture's + * contract. + * + * Schema is applied the faithful way — through Prisma Next's own control + * client (`createPostgresControlClient(...).dbInit({ mode: 'apply' })`), the + * same machinery slice 2's deploy lowering will drive — rather than + * hand-written SQL, so this exercises the real apply-and-sign path end to end. + * (Pulling the control/CLI machinery into the TEST is fine; it never enters + * the app bundle — that isolation is asserted separately in invariants.test.ts.) + * + * Environment-gated: skips cleanly when no Postgres is available locally (no + * `STATE_TEST_DATABASE_URL` and no initdb/pg_ctl), throws on CI. See + * postgres-harness.ts. + */ +import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { createPostgresControlClient } from '@prisma-next/postgres/control'; +import type { Char } from '@prisma-next/target-postgres/codec-types'; +import type { Client } from '../prisma-next.ts'; +import { pnContract, pnPostgres } from '../prisma-next.ts'; +import type { Contract as WidgetContract } from './fixtures/widget-contract/emitted/contract.d.ts'; +import widgetContractJson from './fixtures/widget-contract/emitted/contract.json' with { + type: 'json', +}; +import { startTestPostgres, type TestPostgres } from './postgres-harness.ts'; + +const pg: TestPostgres | undefined = startTestPostgres(); + +if (pg === undefined) { + console.warn( + '[app-cloud] skipping prisma-next integration test: no Postgres available. ' + + 'Set STATE_TEST_DATABASE_URL to point at one, or install initdb/pg_ctl ' + + '(e.g. `brew install postgresql@15`) on PATH.', + ); +} + +describe.skipIf(pg === undefined)('pnPostgres hydrate — live round trip', () => { + if (pg === undefined) return; + const url = pg.url; + + // The wrapped, branded contract — the exact value both the resource and the + // dependency reference. Typing it as WidgetContract makes the hydrated + // client `Client`, so the round-trip below is type-checked. + const contract = pnContract(widgetContractJson); + let migrationsDir: string; + let db: Client; + + beforeAll(async () => { + // Bring the DB's schema to the contract via PN's control client — creates + // the Widget table and writes the contract marker. `migrationsDir` is a + // fresh empty dir: with no authored migrations and no extension spaces, + // dbInit synthesizes the additive create-table plan for the single app + // contract and signs it. + migrationsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prisma-app-pn-mig-')); + const control = createPostgresControlClient({ connection: url }); + await control.connect(); + const result = await control.dbInit({ + contract: widgetContractJson, + mode: 'apply', + migrationsDir, + }); + expect(result.ok).toBe(true); + await control.close(); + + // Construct the client exactly as a service would: through the dependency + // end's hydrate, given only the DB url. + db = await pnPostgres(contract).connection.hydrate({ url }); + }); + + afterAll(async () => { + await db?.close().catch(() => {}); + pg.stop(); + if (migrationsDir !== undefined) fs.rmSync(migrationsDir, { recursive: true, force: true }); + }); + + test('inserts and reads back a Widget row through the typed client', async () => { + // Widget.id is a char(36) column, so its typed value is the branded + // Char<36> — casting here (test files may) is itself proof the client + // is contract-typed: a plain string does not compile as the id. + const id = '11111111-1111-1111-1111-111111111111' as Char<36>; + + const created = await db.orm.public.Widget.create({ id, name: 'gizmo' }); + // Typed result: the ORM row is the contract's Widget shape. Accessing + // `.id`/`.name` compiles only because hydrate returned Client. + expect(created.id).toBe(id); + expect(created.name).toBe('gizmo'); + + const one = await db.orm.public.Widget.where({ id }).first(); + expect(one).not.toBeNull(); + expect(one?.name).toBe('gizmo'); + + const all = await db.orm.public.Widget.all(); + expect(all).toHaveLength(1); + expect(all[0]?.id).toBe(id); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19b6742be..8cde96879 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,6 +292,9 @@ importers: '@prisma-next/cli': specifier: 0.14.0 version: 0.14.0(typanion@3.14.0)(typescript@6.0.3) + '@prisma-next/target-postgres': + specifier: 0.14.0 + version: 0.14.0(typanion@3.14.0)(typescript@6.0.3) '@prisma/app-tsdown': specifier: workspace:0.2.0 version: link:../tsdown-config From 7efe59e17ae64dd855967ad8b6a0f139ebfd3dec Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:35:46 +0200 Subject: [PATCH 10/12] =?UTF-8?q?docs(adr):=20ADR-0021=20=E2=80=94=20data?= =?UTF-8?q?=20deps=20carry=20a=20Prisma=20Next=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the design for the pnPostgres primitive: a Prisma Next-typed postgres resource/dependency whose binding is the contract-typed client, alongside untouched bare postgres(). Consume the contract (emitted artifact), locate the config by path (deploy-only); schema checks are build/deploy-time, so the runtime binding does no verification. Amends ADR-0015 (Prisma Next is framework-blessed like rpc). Includes the Drive project artifacts for the two-slice delivery. Signed-off-by: willbot Signed-off-by: Will Madden --- .../prisma-next-data-contract/design-notes.md | 154 ++++++++++++++++++ .../prisma-next-data-contract/plan.md | 64 ++++++++ .../slices/typed-primitive/spec.md | 126 ++++++++++++++ .../prisma-next-data-contract/spec.md | 102 ++++++++++++ ...-data-deps-carry-a-prisma-next-contract.md | 144 ++++++++++++++++ docs/design/90-decisions/README.md | 1 + 6 files changed, 591 insertions(+) create mode 100644 .drive/projects/prisma-next-data-contract/design-notes.md create mode 100644 .drive/projects/prisma-next-data-contract/plan.md create mode 100644 .drive/projects/prisma-next-data-contract/slices/typed-primitive/spec.md create mode 100644 .drive/projects/prisma-next-data-contract/spec.md create mode 100644 docs/design/90-decisions/ADR-0021-data-deps-carry-a-prisma-next-contract.md diff --git a/.drive/projects/prisma-next-data-contract/design-notes.md b/.drive/projects/prisma-next-data-contract/design-notes.md new file mode 100644 index 000000000..23a309af5 --- /dev/null +++ b/.drive/projects/prisma-next-data-contract/design-notes.md @@ -0,0 +1,154 @@ +# Design notes — Prisma Next data contract + +Settled in an operator design session (2026-07-11). The ADR draft +(`docs/design/90-decisions/ADR-0021-*.md`) is the durable record; these notes +carry the working detail and the deferred extension design. + +## Principles + +- **Opt-out stays real.** Bare `postgres()` is untouched — the `any` of data + deps, same role `http()` plays for communication. Prisma Next is a second, + parallel primitive; a project or service that doesn't import it never loads + `@prisma-next/*` or `pg` at runtime. +- **Schema checking is build/deploy-time, not runtime.** The deploy is the + authoritative check (migrate to the contract hash or fail). The runtime + binding does no schema verification — it just builds the client — so a + running service can never be crashed by a marker check. Runtime marker + verification is explicitly disabled, not warn-only. +- **Deploys never run synthesized plans.** The deploy step migrates along the + authored migration graph only, and fails if the graph has no path to the + target hash. `dbUpdate`-style diff-and-apply is not used against deployed + databases. + +## The model + +Two ends, one contract: + +```ts +// resource end — the owning system +import contractJson from './contract.json' // consume: data +import type { Contract } from './contract.d' // consume: types +const contract = pnContract(contractJson) +const db = provision('database', pnPostgres({ + name: 'database', + contract, + config: './prisma-next.config.ts', // locate: a PATH string, deploy-only +})) + +// dep end — each consumer +deps: { db: pnPostgres(contract) } // binding: PostgresClient +``` + +- **Consume the contract; locate the config.** The resource carries two + different things by two different doors. The **contract** enters as its + emitted artifact (`contract.json` + `contract.d.ts`) — consumed by the + runtime and the type system, lightweight, safe to bundle. The + **`prisma-next.config.ts`** enters as a **path string** — the deploy + migration step reads it to find the migrations directory. The app build + **never imports** the config; importing it pulls PN's CLI / migration engine + / source-providers into the bundle. `connection` is gone entirely — there is + no config object to carry a connection on. +- **Single contract per database (v1).** The user authors one contract that + serves all consuming systems. Every consumer sees the full contract type — + no least-privilege slices yet. +- **Compile-time check:** the dep's `required` contract and the resource's + provided contract are the same emitted `Contract` type; the branded + `storageHash` literal makes assignability exact-version equality. + `satisfies()` mirrors it at Load as a hash-equality check. +- **Binding is the typed client**, constructed in hydrate: + `postgres({ contractJson, url })` from + `@prisma-next/postgres/runtime` — no `verifyMarker` (no runtime schema + check). Lazy pool, rides node-postgres. This amends ADR-0015's "pack ships + no driver" for this dep kind: Prisma Next is framework-blessed like rpc, so + the contract alone *can* construct the client. +- **Type flow by authoring mode:** TS-authored contracts carry their own types + through the config import; PSL-first passes the emitted `contract.d.ts` type + explicitly as the type parameter. Support and document both. + +## Deploy lowering + +Per PN-postgres resource, after DB provisioning, an Alchemy resource: + +1. `readMarker()` on the live DB → compare marker `storageHash` to the + contract's. +2. Equal → no-op (idempotent redeploy). +3. Different → PN `migrate`: walk the authored migration graph from the + marker's hash to the target hash. Resume-safe; marker writes are atomic + with apply. +4. Fail the deploy on: no path through the graph, destructive step without + explicit opt-in (`acceptDataLoss` stays off), or runner failure. A failed + apply leaves marker and diff unchanged. + +The plan-mode operation list is the Alchemy diff preview. Migration files are +read from disk relative to the config — deploys run from a machine/CI that has +the workspace, so the files are present where the lowering runs. + +## Packaging + +In `@prisma/app-cloud` behind a dedicated subpath entry +(`@prisma/app-cloud/prisma-next`), not re-exported from the index — so the +`@prisma-next/postgres` + `pg` dependency tree loads only when imported. +Install weight is shared; runtime weight isn't. (Operator: no new npm +packages. Folding `@prisma/app-rpc` into app-cloud is a separate move, out of +scope here.) + +## Deferred: the multi-contract / contract-space model + +Worked out in the design session; deferred because PN's multi-peer-app-space +support (ADR 212's "monorepo aggregator" case) has unproven edges. When picked +up: + +- Resource declares the set of contracts it hosts: + `pnPostgres({ name, contracts: [sales, auth] })` — the aggregate in code; + the owner consents to each slice; wiring typechecks membership. +- Each consumer's slice maps to a PN **contract space**; disjointness is + PN-verified (`STORAGE_ELEMENT_CONFLICT`); apply ordering is PN's. +- **Space id must live on the contract declaration, never derived from + topology names** — the id is what links versions of a contract over time + (marker at hash X is this contract's predecessor, not a stranger claiming + overlapping tables). Deriving from a system/dep name makes renames read as + conflicts. +- Prerequisite spike: confirm PN handles multiple peer app-authored spaces in + one database end-to-end. + +## Deferred: dev-time + +Parked. Intended shape: `prisma dev` serves identical copies of the management +API locally; Alchemy then treats the local machine as one more deploy target +and the lowering doesn't change at all. Nothing to design in this project. + +## Alternatives considered + +- **Binding = `{ url, contractJson }`, app constructs the client** — keeps + packs driver-free (ADR-0015 as written) at the cost of one line of app code + and the framework not constructing the typed client. Rejected: "data + contracts are Prisma Next" is a README-level framework decision; PN is + blessed the way rpc is. +- **Sibling npm package for the primitive** — cleanest dependency isolation, + rejected by operator (package proliferation); subpath entry achieves the + runtime isolation that matters. +- **Deriving the aggregate from wiring** (no contracts on the resource + declaration) — superseded by single-contract v1; in the multi-contract + extension the explicit declaration wins anyway (owner consent, visible + edits, lowering reads the node not the graph). +- **`dbUpdate` at deploy** — synthesized plans against production databases; + rejected outright. Authored `migrate` only. + +## Open questions + +- Factory name. `pnPostgres` is a placeholder; PN becomes **Prisma Data** at + GA, so the name will churn. Decide before slice 1 merges. +- `@prisma-next/*` is 0.x (0.14.0 on npm) and fast-moving — pinning strategy + and breakage tolerance. +- Exact `verifyMarker` warn-only semantics — confirm PN's option supports + warn-don't-throw, or wrap it. + +## References + +- ADR draft: `docs/design/90-decisions/ADR-0021-*.md` +- PN surfaces: `@prisma-next/postgres` `/contract-builder`, `/runtime`, + `/control`, `/config`; contract spaces ADR 212; `migrate` / + `readMarker` / `readLedger` in the control API. +- Framework surfaces: `packages/app-cloud/src/postgres.ts` (bare primitive), + `packages/app-cloud/src/control.ts` (lowering), ADR-0013/0015 (dep model, + bindings). diff --git a/.drive/projects/prisma-next-data-contract/plan.md b/.drive/projects/prisma-next-data-contract/plan.md new file mode 100644 index 000000000..15b42544e --- /dev/null +++ b/.drive/projects/prisma-next-data-contract/plan.md @@ -0,0 +1,64 @@ +# Project plan — Prisma Next data contract + +## Summary + +Two slices in a stack. Slice 1 lands the ADR and the typed primitive with its +runtime behavior proven against a real local Postgres; slice 2 lands the +deploy-time migration step and proves the whole path live on Prisma Cloud via +an example conversion. + +**Spec:** `.drive/projects/prisma-next-data-contract/spec.md` + +## Slices + +### Slice 1: typed primitive (`typed-primitive`) + +**Outcome:** `pnPostgres` (name settled by then) exists in +`@prisma/app-cloud/prisma-next`: resource end takes `{ name, config }` from a +`prisma-next.config.ts` import; dep end resolves to a +`PostgresClient` built in hydrate with warn-only marker +verification; compile-time assignability is exact on `storageHash` and +`satisfies()` mirrors it. ADR-0021 ships in this PR. + +**Slice DoD:** unit tests for contract assignability/`satisfies`; integration +test against a real local Postgres (existing harness) proving the typed +client round-trips and a mismatched marker warns without throwing; bare +`postgres()` suite untouched; subpath entry verified not to load +`@prisma-next/*` from the index import. + +**Builds on:** — +**Hands to:** stable primitive + `Contract` type flow + ADR merged, for the +lowering to target. + +**Linear:** [TML-3009](https://linear.app/prisma-company/issue/TML-3009/slice-1-typed-pnpostgres-primitive-adr-0021) + +### Slice 2: deploy migrate lowering (`deploy-migrate-lowering`) + +**Outcome:** the app-cloud control extension lowers a PN-postgres resource to +DB provisioning plus a migration step: read marker → authored `migrate` to +the contract hash → hard fail on no-path/destructive/runner error, no-op when +hashes match. An example app (storefront-auth converted, or a sibling +example) authors a contract + migration and deploys live; CI E2E covers +deploy, migrated redeploy, and no-op redeploy. + +**Slice DoD:** live Prisma Cloud round trip through the typed client; +no-path deploy failure test leaves DB untouched; at least one CI example +still exercises bare `postgres()`. + +**Builds on:** Slice 1. +**Hands to:** project DoD; the datahub port consumes the shipped primitive. + +**Linear:** _pending green light_ + +## Sequencing + +Stack: 1 → 2. No parallel groups — slice 2 consumes slice 1's primitive. + +## Close-out (required) + +- [ ] Verify all acceptance criteria in `spec.md` +- [ ] Migrate long-lived docs into `docs/` (ADR-0021 already lands in slice 1; + migrate the deferred multi-contract design from design-notes into + docs/design if not already captured in the ADR) +- [ ] Strip repo-wide references to `.drive/projects/prisma-next-data-contract/**` +- [ ] Delete `.drive/projects/prisma-next-data-contract/` diff --git a/.drive/projects/prisma-next-data-contract/slices/typed-primitive/spec.md b/.drive/projects/prisma-next-data-contract/slices/typed-primitive/spec.md new file mode 100644 index 000000000..cc892ffc2 --- /dev/null +++ b/.drive/projects/prisma-next-data-contract/slices/typed-primitive/spec.md @@ -0,0 +1,126 @@ +# Slice 1 spec — typed pnPostgres primitive + +**Linear:** TML-3009 · **Project:** prisma-next-data-contract + +## Outcome + +A Prisma Next-typed postgres primitive exists in `@prisma/app-cloud` behind a +dedicated subpath entry, with its static surface and its runtime hydrate both +proven — unit/type tests plus a live local-Postgres round trip. ADR-0021 ships +in the same PR. + +## Scope + +**In:** +- `packages/app-cloud/src/prisma-next.ts` — the new module: a + `Contract<'prisma-next', …>` kind, the `pnPostgres` factory (resource + + dependency overloads, mirroring `rpc.ts`), hydrate constructing the Prisma + Next client with no runtime schema verification (checks are deploy-time). +- `packages/app-cloud/package.json` — add `@prisma-next/postgres` dependency + and a `./prisma-next` subpath export; `tsdown.config.ts` — add the entry. +- Test fixtures: a small Prisma Next contract + its `prisma-next.config.ts` + and emitted artifacts (or TS no-emit contract), under the package's test + tree. +- Unit + type tests; one integration test against the repo's local-Postgres + harness. + +**Out:** +- The deploy lowering in `control.ts` (slice 2). +- Any example-app conversion (slice 2). +- Bare `postgres()` — untouched. +- Multi-contract / contract-space support. + +## Design decisions locked (operator, post-D1r1) + +- **Resource shape: `pnPostgres({ name, contract })`.** The contract is the + *consumed* emitted artifact. The `prisma-next.config.ts` **path** (which the + deploy migration step needs) is **not** part of D1 — it rides on the resource + in slice 2, alongside the lowering that reads it (the `ResourceNode` has no + metadata slot today; that mechanism is a slice-2 decision). Never import the + config into the app build. +- **No runtime schema verification.** Hydrate builds the client with no + `verifyMarker` — schema checking is build/deploy-time only. A running + service can't be crashed by a marker check because there is no marker check. + +## Slice DoD + +- [ ] `pnPostgres` resource (`{ name, contract }`) + dependency overloads + typecheck; the dependency's binding is the Prisma Next client typed by + the contract. +- [ ] Type test: a consumer requiring contract vX is assignable to a resource + providing vX, and a different `storageHash` is a type error. +- [ ] Unit test: `satisfies()` returns true for equal `storageHash`, false + otherwise **including the missing/malformed-hash paths** (reviewer + finding — these branches are correct but untested); factory returns + correct `ResourceNode` / `DependencyEnd` shapes. +- [ ] Integration test (local Postgres): the hydrated client round-trips a + query against a DB migrated to the contract. (No mismatched-marker case — + there is no runtime check.) +- [ ] `@prisma/app-cloud` index import does not pull in `@prisma-next/*`/`pg` + (verified: the symbol lives only behind `./prisma-next`). +- [ ] Bare `postgres()` unit/type tests unchanged and green. +- [ ] Validation gate green (below). + +## Validation gate + +- `pnpm --filter @prisma/app-cloud typecheck` +- `pnpm --filter @prisma/app-cloud test` (bun test) +- `pnpm --filter @prisma/app-cloud test:types` (vitest --typecheck) +- Integration test command for the local-Postgres test (implementer confirms + the harness invocation; the state-store harness self-spawns `postgresql@15` + or reads `STATE_TEST_DATABASE_URL`). + +## Open questions carried from the project spec + +- Factory name (`pnPostgres` is the working name; do not block on it — the + operator settles it before merge). +- ~~Runtime `verifyMarker` semantics~~ — **resolved / moot.** The operator + ruled out runtime schema verification entirely (checks are build/deploy-time + only). Hydrate carries no `verifyMarker`. + +## Dispatch plan + +### D1 — primitive + unit/type proof + +**Outcome:** `pnPostgres` compiles and is unit/type-proven without a live DB — +package wired, contract kind + factory + hydrate implemented, subpath export +in place, bare-postgres path untouched. + +**Builds on:** — · **Hands to:** a compiling, unit-proven primitive whose +hydrate constructs the PN client (lazy — no connection yet). + +**Focus:** structure mirrors `packages/app-rpc/src/rpc.ts`. The PN client is +lazy (pool on first query), so hydrate is fully implementable and unit-testable +without Postgres. Resolve the `verifyMarker` open question here. + +**Completed when:** typecheck + `bun test` + `vitest --typecheck` green for +`@prisma/app-cloud`; type test proves storageHash-exact assignability; unit +test proves `satisfies` + node shapes; index import free of `@prisma-next/*`. + +### D1r2 — reshape + coverage (batched: operator steer + reviewer finding) + +**Outcome:** the primitive matches the locked design — resource is +`{ name, contract }`, hydrate has no `verifyMarker`, and the `satisfies` +missing/malformed-hash branches are tested. + +**Builds on:** D1r1. · **Hands to:** D2. + +**Completed when:** resource overload is `{ name, contract }` (no +`PnPostgresConfig`/`connection`); hydrate builds the client with no +`verifyMarker`; new unit cases cover `satisfies` on missing-hash / +malformed-`__cmp` both directions; module doc updated; full gate green. + +### D2 — live integration proof + +**Outcome:** the hydrated client is proven against a real local Postgres — +round-trips a query on a DB migrated to the contract. + +**Builds on:** D1r2's primitive. · **Hands to:** slice DoD; slice 2's lowering. + +**Focus:** use the repo's local-Postgres harness. Needs the fixture contract +migrated into the DB (apply it via PN's control client in test setup, or seed +the schema directly). Prove the round-trip only — there is no runtime marker +check to exercise. + +**Completed when:** integration test green against a real DB (round-trip); +full validation gate green. diff --git a/.drive/projects/prisma-next-data-contract/spec.md b/.drive/projects/prisma-next-data-contract/spec.md new file mode 100644 index 000000000..fae331c22 --- /dev/null +++ b/.drive/projects/prisma-next-data-contract/spec.md @@ -0,0 +1,102 @@ +# Project spec — Prisma Next data contract + +## Purpose + +Give app authors a typed, migration-verified data boundary: a data dependency +that carries a Prisma Next contract, so the injected client is typed to the +schema and every deploy converges the database to the contract's hash or +fails. This delivers the promise already written into the framework's design +("data contracts are Prisma Next" — README, glossary) that the code has never +implemented. + +## At a glance + +- A second data primitive (`pnPostgres`, name TBD) alongside bare + `postgres()`: resource end takes the app's `prisma-next.config.ts` and + contract; dep end resolves to a Prisma Next client typed by that contract. +- Compile-time exactness via the contract's branded `storageHash`; Load-time + `satisfies()` mirrors it. +- Deploy lowering gains a migration step: read the DB's marker, `migrate` + along the authored graph to the target hash, hard-fail if no path. +- Proven live on an example app deployed to Prisma Cloud. + +## Non-goals + +- **Multi-contract / contract-space slices.** v1 is one contract per + database; every consumer sees the full contract. The slice/aggregate design + is recorded in design-notes as the extension path. +- **Least-privilege data access.** Follows from the above; not in v1. +- **Dev-time apply/emulation.** Parked pending `prisma dev` serving the + management API locally (Alchemy then gets a second target for free). +- **Changing or deprecating bare `postgres()`.** It remains the untyped + escape hatch, unchanged. +- **State-store adoption of Prisma Next.** ADR-0012's deferral stands. +- **The Prisma Next → Prisma Data rename.** Naming churn is acknowledged, not + handled here. +- **Folding `@prisma/app-rpc` into app-cloud.** Operator floated it; separate + mechanical move. + +## Place in the larger world + +- Fulfils the README/glossary Data Contract promise and amends ADR-0015: + Prisma Next becomes a framework-blessed protocol (like rpc), so this dep + kind's binding is a typed client, not `{ url }`. +- Direct consumer: the forcing-function apps stream (datahub port), which + needs real typed persistence. +- Depends on published `@prisma-next/*` (0.14.0 on npm): `/contract-builder`, + `/runtime`, `/control`, `/config` entry points. + +## Cross-cutting requirements + +- **Opt-out is real at runtime.** The primitive ships in `@prisma/app-cloud` + behind a dedicated subpath entry, never re-exported from the index; a + service that doesn't import it never loads `@prisma-next/*`/`pg`. +- **Schema checking is build/deploy-time, not runtime.** The runtime binding + does no schema verification — it just builds the client — so a running + service can never be crashed by a marker check. The authoritative check is + the deploy. +- **Consume the contract; locate the config.** The app build imports only the + emitted contract (`contract.json` + `contract.d.ts`), never + `prisma-next.config.ts` (which would pull PN's CLI/migration machinery into + the bundle). The config reaches the deploy lowering as a **path string**. +- **Deploys never synthesize schema changes.** Authored `migrate` only; + `acceptDataLoss` off; no-path and destructive plans are hard deploy + failures that leave marker and DB unchanged. +- **No-globals holds.** The framework injects the connection URL at hydrate. + +## Transitional-shape constraints + +- Additive only: no change to the existing `postgres()` contract, `Contract` + machinery, or `control.ts` behavior for bare-postgres resources at any + intermediate state. +- At least one CI example keeps exercising bare `postgres()` end-to-end after + the example conversion, so the untyped path stays covered. + +## Project DoD + +- [ ] An example app declaring a Prisma Next contract deploys to Prisma Cloud + and serves a request that round-trips through the typed client. +- [ ] A contract change with an authored migration deploys and migrates the + live DB; an unchanged redeploy is a no-op (CI no-op-redeploy check + extended to cover the marker comparison). +- [ ] A deploy targeting a hash with no authored path fails with a typed + error and leaves the database untouched (test-verified). +- [ ] A service running against a DB with a mismatched marker logs a warning + and still serves (test-verified). +- [ ] Bare-`postgres()` examples and tests are untouched and green. +- [ ] ADR-0021 accepted and merged; design-notes' deferred-extension content + preserved there or in docs/design. + +## Open questions + +- Factory name (`pnPostgres` placeholder; Prisma Data rename incoming). +- `@prisma-next/*` version pinning strategy given 0.x churn. +- Whether PN's `verifyMarker` option natively supports warn-only, or the + hydrate wraps it. (Resolve in slice 1 against the real API.) + +## References + +- `.drive/projects/prisma-next-data-contract/design-notes.md` +- `docs/design/90-decisions/ADR-0021-*.md` (draft) +- `docs/design/90-decisions/ADR-0013…0015`, `docs/design/03-domain-model/glossary.md` §Contracts +- `packages/app-cloud/src/{postgres,control}.ts` diff --git a/docs/design/90-decisions/ADR-0021-data-deps-carry-a-prisma-next-contract.md b/docs/design/90-decisions/ADR-0021-data-deps-carry-a-prisma-next-contract.md new file mode 100644 index 000000000..0049023f3 --- /dev/null +++ b/docs/design/90-decisions/ADR-0021-data-deps-carry-a-prisma-next-contract.md @@ -0,0 +1,144 @@ +# ADR-0021: Data deps carry a Prisma Next contract; deploys migrate to its hash + +## Status + +Proposed + +## Decision + +A second data primitive joins bare `postgres()`: a Prisma Next-typed postgres +resource and dependency (working name `pnPostgres`, final name TBD), shipped +in `@prisma/app-cloud` behind a dedicated subpath entry. + +- The **resource end** takes two separate things: the **contract**, consumed + as its emitted artifact (`contract.json` data + `contract.d.ts` types — pure + data and types, no Prisma Next deploy machinery), and the app's + `prisma-next.config.ts` **by path** (a plain string). The contract types and + wires the resource; the config path is deploy-only metadata the migration + step reads. The app build **never imports** `prisma-next.config.ts` — + importing it would pull Prisma Next's CLI, migration engine, and + source-providers into the user's bundle. One contract per database. +- The **dep end** requires the same `Contract` and resolves at `load()` to a + typed Prisma Next client — `postgres({ contractJson, url })` — + constructed by the framework in hydrate. The binding just builds the client; + there is **no runtime schema verification** (see below). +- Compile-time wiring is exact: the contract's branded `storageHash` literal + makes assignability version-equality, and `satisfies()` mirrors it at Load + as a hash check. +- The deploy lowering gains a migration step per Prisma Next postgres + resource: read the live database's contract marker; if its `storageHash` + differs from the contract's, walk the **authored** migration graph + (`migrate`) to the target hash; fail the deploy if no path exists, if a + step is destructive without explicit opt-in, or if the runner fails. + Synthesized diff-and-apply (`dbUpdate`) is never used against a deployed + database. + +Bare `postgres()` is unchanged: the untyped escape hatch, the `any` of data +deps, the same role `http()` plays for communication. + +## Reasoning + +The design has promised this since the README was written — a data Input +carries "a Prisma Next description of exactly the tables and columns," and the +glossary defines the Data Contract as a hashable, deterministic schema slice — +but nothing in the code implements it: today's binding is `{ url }` and +`satisfies` compares kind only. Prisma Next's actual surface fits the promise +with almost no adaptation. Contracts are committed artifacts (`contract.json` ++ `contract.d.ts`) whose branded `storageHash` type puts the schema version in +the type system; the runtime is generic over the emitted type with no client +codegen and accepts an explicit connection string, which matches injected +bindings; the control API exposes exactly the marker-read/plan/migrate +operations the deploy step needs. + +**The binding is a typed client, which amends ADR-0015 rather than violating +it.** ADR-0015's principle is that a binding is the most-derived thing the +contract alone can construct; it lands on `{ url }` for bare postgres because +constructing more would bless a driver. A dep that carries a Prisma Next +contract *can* construct the typed client from the contract alone — contract +plus URL is the client's entire input. "Data contracts are Prisma Next" is a +framework-level decision (README, guiding principles), so Prisma Next is +blessed the way rpc is, and this dep kind gets a client while bare postgres +keeps `{ url }`. The dependency cost is contained by packaging: the primitive +lives behind its own subpath entry, never re-exported from the index, so a +service that opts out never loads `@prisma-next/postgres` or `pg` at runtime. + +**Consume the contract; locate the config.** The two things the resource +needs pull in opposite directions, so they enter by different doors. The +runtime and the type system only need to *consume* the contract — the emitted +`contract.json` (data hydrate hands the runtime) and `contract.d.ts` (types), +both lightweight, importable into the app build with no deploy machinery +attached. The deploy migration step needs to *locate* the config — the +`prisma-next.config.ts` from which Prisma Next resolves the migrations +directory — but it needs only the **path**, a string, read at deploy time. +Passing the config as a path rather than an import is what keeps Prisma Next's +CLI and migration engine out of the user's bundle while still giving the +deploy lowering what it needs. A single contract is Prisma Next's mainline +single-space model — no dependency on multi-space edge cases — and the user is +responsible for authoring one contract that serves all consuming systems. +Every consumer sees the full contract type; least-privilege slices are +deferred (see Alternatives). + +**Schema checking is a build/deploy-time job, not a runtime one.** The +authoritative check is the deploy: `migrate` walks the authored graph from the +marker's hash to the target, is resume-safe, and writes markers atomically +with apply, so a failed deploy leaves marker and database unchanged; a contract +with no authored path is a deploy failure surfaced before any DB change. +Synthesized plans are a dev-time convenience, never a production operation. +Because the deploy guarantees the live database is at the contract's hash, the +runtime binding does **no** schema verification — it just builds the client. +That is deliberate: it keeps schema correctness in the one place that can +actually enforce it, and it means a running service can never be crashed (nor +meaningfully warned) by a runtime marker check. The framework injects the +connection URL at hydrate (no-globals). + +## Consequences + +- Services get schema-typed data access with the schema version enforced at + the type level, at Load, and at deploy — three checkpoints, same shape as + rpc contracts. +- `@prisma/app-cloud` takes `@prisma-next/postgres` (and transitively `pg`) + as a dependency; install weight is shared by all users, runtime weight only + by importers of the subpath. +- The deploy pipeline becomes schema-aware: contract changes without an + authored migration path are deploy failures, surfaced before any DB change. +- v1 has no per-consumer data slices: shared databases expose the whole + contract to every consumer. The topology still shows each data edge. +- The Prisma Next → Prisma Data rename will churn the primitive's name; + accepted. +- ADR-0015 is amended as described; ADR-0012 (state store stays plain SQL) is + untouched. + +## Alternatives considered + +- **Binding = `{ url, contractJson }`, client constructed app-side** — keeps + ADR-0015 as written and packs driver-free, at the cost of the framework not + constructing the least-privilege client it has every input for. Rejected: + Prisma Next is framework-blessed, not an arbitrary driver. +- **Multi-contract resource declarations mapped to Prisma Next contract + spaces** — the fuller model: the resource declares the set of contracts it + hosts (the aggregate, in code), each consumer's slice is a contract space, + disjointness is Prisma Next-verified, and the space id lives on the + contract declaration (never derived from topology names, so renames don't + read as conflicts). Deferred, not rejected: it depends on multi-peer-app- + space support whose edges are unproven, and v1's single contract needs none + of it. Recorded as the extension path. +- **Deriving a database's contract set from wiring** instead of declaring it — + superseded by the single-contract cut; in the multi-contract extension the + explicit declaration wins anyway (owner consent, visible edits). +- **`dbUpdate` (synthesized plans) at deploy** — rejected outright; authored + migrations are the only production schema path. +- **A sibling npm package for the primitive** — cleanest isolation, rejected + for package proliferation; the subpath entry achieves the isolation that + matters. + +## Related + +- [ADR-0015](ADR-0015-dependencies-resolve-to-bindings-clients-are-app-side.md) — + amended: Prisma Next joins the protocol-owned kinds whose binding is a client. +- [ADR-0013](ADR-0013-resources-are-provisioned-by-systems-deps-are-declarations.md) — + the slot model this plugs into. +- [ADR-0012](ADR-0012-the-state-store-speaks-sql-directly.md) — unaffected; + the state store's deferral stands. +- `docs/design/03-domain-model/glossary.md` § Data Contract / Aggregate + Contract — the promised semantics; the aggregate becomes real in the + deferred multi-contract extension. diff --git a/docs/design/90-decisions/README.md b/docs/design/90-decisions/README.md index c59b00b41..93cded20b 100644 --- a/docs/design/90-decisions/README.md +++ b/docs/design/90-decisions/README.md @@ -33,3 +33,4 @@ _Earlier drafts (ADR-0001, ADR-0002) were retired as the high-level design settl - [ADR-0015](ADR-0015-dependencies-resolve-to-bindings-clients-are-app-side.md) — Dependencies resolve to bindings (a client for protocol-owned kinds, typed config for resources); clients are constructed app-side. - [ADR-0016](ADR-0016-a-system-has-the-same-boundary-as-a-service.md) — A system has the same boundary as a service: deps in, expose out, forwarding as data flow; reusable systems follow. - [ADR-0017](ADR-0017-control-plane-loads-through-the-app-config.md) — Control-plane code loads through `prisma-app.config.ts`: the config statically imports extension descriptors; registries are keyed by (extension ID, node ID); nodes are pure data; one explicit state store per deploy. +- [ADR-0021](ADR-0021-data-deps-carry-a-prisma-next-contract.md) — Data deps carry a Prisma Next contract: typed client binding, one contract per database via its config, deploys migrate along authored edges to the contract hash. *(Proposed)* From 8738f19a8d4ed8e6178963ee8cd7d5eba8c68c63 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:52:44 +0200 Subject: [PATCH 11/12] test(app-cloud): author the gadget fixture in PSL, keeping widget as TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review ask on PR #44 ("use PSL"): the gadget contract is now authored as a .prisma file instead of TS defineContract(), and its config points at the .prisma source (same defineConfig({ contract: }) shape — the provider picks PSL from the extension). Widget stays TS-authored on purpose: widget covers the TS defineContract() authoring mode, gadget covers PSL, so the two fixtures exercise both modes ADR-0021 says to support. A short mode note is added to each source. The PSL re-expresses the identical schema (id char(36) uuid(4) default @id, label text), with @@map("Gadget") to keep the physical table capitalized — PSL lowercases table names by default, and that was the only thing that differed. Because storageHash is a deterministic fingerprint of the schema, not the authoring form, the re-emitted emitted/ artifacts are byte-for-byte identical (storageHash sha256:35caf6…) — so the widget-vs-gadget type test and everything consuming emitted/ stay green with no test edits. Signed-off-by: willbot Signed-off-by: Will Madden --- .../gadget-contract/source/contract.prisma | 15 +++++++++++ .../gadget-contract/source/contract.ts | 15 ----------- .../source/prisma-next.config.ts | 5 +++- .../widget-contract/source/contract.ts | 26 +++++++++---------- 4 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.prisma delete mode 100644 packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.ts diff --git a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.prisma b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.prisma new file mode 100644 index 000000000..dacdea8e0 --- /dev/null +++ b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.prisma @@ -0,0 +1,15 @@ +// use prisma-next +// +// PSL-authored fixture: the gadget contract covers Prisma Next's PSL-first +// authoring mode (the widget fixture covers the TS `defineContract()` mode), +// so the two together exercise both modes ADR-0021 says to support. This +// schema is deliberately identical to what the old TS source produced — +// `@@map("Gadget")` keeps the physical table capitalized (PSL lowercases by +// default) so the emitted storageHash matches byte-for-byte. + +model Gadget { + id String @id @default(uuid(4)) + label String + + @@map("Gadget") +} diff --git a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.ts b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.ts deleted file mode 100644 index 14042cbba..000000000 --- a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/contract.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineContract } from '@prisma-next/postgres/contract-builder'; - -export const contract = defineContract( - { extensionPacks: {} }, - ({ field: f, model: m }) => ({ - models: { - Gadget: m('Gadget', { - fields: { - id: f.id.uuidv4String(), - label: f.text(), - }, - }), - }, - }), -); diff --git a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/prisma-next.config.ts b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/prisma-next.config.ts index e1e2c59f9..2f065f0cb 100644 --- a/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/prisma-next.config.ts +++ b/packages/app-cloud/src/__tests__/fixtures/gadget-contract/source/prisma-next.config.ts @@ -1,6 +1,9 @@ import { defineConfig } from '@prisma-next/postgres/config'; +// PSL-first: the contract source is a `.prisma` file. `defineConfig` picks the +// PSL provider from the `.prisma` extension — same `{ contract: }` shape +// the widget fixture uses to point at its TS source. export default defineConfig({ - contract: './contract.ts', + contract: './contract.prisma', db: { connection: 'postgres://localhost:5432/placeholder' }, }); diff --git a/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/contract.ts b/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/contract.ts index f14040d1e..86730f9ae 100644 --- a/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/contract.ts +++ b/packages/app-cloud/src/__tests__/fixtures/widget-contract/source/contract.ts @@ -1,15 +1,15 @@ +// TS-authored fixture: the widget contract covers Prisma Next's TS no-emit +// `defineContract()` authoring mode (the gadget fixture covers the PSL-first +// mode), so the two together exercise both modes ADR-0021 says to support. import { defineContract } from '@prisma-next/postgres/contract-builder'; -export const contract = defineContract( - { extensionPacks: {} }, - ({ field: f, model: m }) => ({ - models: { - Widget: m('Widget', { - fields: { - id: f.id.uuidv4String(), - name: f.text(), - }, - }), - }, - }), -); +export const contract = defineContract({ extensionPacks: {} }, ({ field: f, model: m }) => ({ + models: { + Widget: m('Widget', { + fields: { + id: f.id.uuidv4String(), + name: f.text(), + }, + }), + }, +})); From 768777f7026feaa40bb0a3ee7fc351201916a6e2 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 11 Jul 2026 19:59:07 +0200 Subject: [PATCH 12/12] chore(lint): ignore generated PN fixture artifacts; drop an import-alias as MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes so the CI Lint + cast-ratchet jobs pass on this branch: - biome.jsonc: ignore `**/fixtures/**/emitted` — that dir holds only generated `prisma-next contract emit` output (contract.json plus the already-ignored contract.d.ts), which must stay byte-exact and must not be reformatted. Uses the folder form without a trailing `/**` (biome's own useBiomeIgnoreFolder rule rejects the `/**` suffix since 2.2.0). - prisma-next.ts: the `no-bare-cast` grit rule counts every `as` token, so the `Contract as PnRawContract` import alias (a rename, not a cast) tripped the cast-ratchet as one net-new cast vs merge-base. Replaced the aliased import with an inline `import('@prisma-next/contract/types').Contract<…>` type reference — no `as`, no logic change. Ratchet is back to delta=0. Signed-off-by: willbot Signed-off-by: Will Madden --- biome.jsonc | 1 + packages/app-cloud/src/prisma-next.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 0192a9db2..9d299687b 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -20,6 +20,7 @@ "!**/.prisma", "!**/.alchemy", "!**/*.d.ts", + "!**/fixtures/**/emitted", "!**/pnpm-lock.yaml" ] }, diff --git a/packages/app-cloud/src/prisma-next.ts b/packages/app-cloud/src/prisma-next.ts index bfac2f13a..668243371 100644 --- a/packages/app-cloud/src/prisma-next.ts +++ b/packages/app-cloud/src/prisma-next.ts @@ -27,7 +27,6 @@ import type { Contract, DependencyEnd, ResourceNode } from '@prisma/app'; import { dependency, resource } from '@prisma/app'; -import type { Contract as PnRawContract } from '@prisma-next/contract/types'; import pnPostgresRuntime, { type PostgresClient } from '@prisma-next/postgres/runtime'; import type { SqlStorage } from '@prisma-next/sql-contract/types'; @@ -36,7 +35,7 @@ import type { SqlStorage } from '@prisma-next/sql-contract/types'; * authoring modes (TS no-emit `defineContract()`, or PSL/emitted * `contract.d.ts`) satisfy. */ -export type AnyPnContract = PnRawContract; +export type AnyPnContract = import('@prisma-next/contract/types').Contract; /** * The comparison payload behind a `prisma-next` Contract. `contractJson` is