From 0c5e296da95c7e755a6794dd74078f721d67bc2f Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 8 Feb 2025 00:02:06 -0800 Subject: [PATCH 01/13] chore: upgrade upload-artifact CI action --- .github/workflows/security-scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-scorecard.yml b/.github/workflows/security-scorecard.yml index 2040044d7..c80226279 100644 --- a/.github/workflows/security-scorecard.yml +++ b/.github/workflows/security-scorecard.yml @@ -55,7 +55,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: 'Upload artifact' - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4 with: name: SARIF file path: results.sarif From 94dd30f4cc5c53c01d6747825c39df60d75c5dc4 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 20 Feb 2025 20:12:55 -0800 Subject: [PATCH 02/13] chore: updating prisma dependencies to 6.4.x (#1995) --- .../tests/projects/nuxt-trpc-v10/package.json | 4 +- .../tests/projects/nuxt-trpc-v11/package.json | 4 +- .../tests/projects/t3-trpc-v11/package.json | 4 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 4 +- packages/sdk/package.json | 4 +- pnpm-lock.yaml | 137 +++++++++++------- script/test-scaffold.ts | 2 +- tests/integration/test-run/package.json | 4 +- tests/integration/tests/cli/plugins.test.ts | 4 +- .../nextjs/test-project/package.json | 4 +- .../frameworks/trpc/test-project/package.json | 4 +- 12 files changed, 101 insertions(+), 76 deletions(-) diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json index f37b3eb21..c1c95fa8d 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json @@ -10,7 +10,7 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "@prisma/client": "6.3.x", + "@prisma/client": "6.4.x", "@trpc/client": "^10.45.2", "@trpc/server": "^10.45.2", "nuxt": "^3.14.1592", @@ -21,7 +21,7 @@ }, "devDependencies": { "esbuild": "^0.24.0", - "prisma": "6.3.x", + "prisma": "6.4.x", "typescript": "^5.6.2", "vue-tsc": "^2.1.10" } diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json index a9463e0d0..e6e65dc80 100644 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json +++ b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json @@ -10,7 +10,7 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "@prisma/client": "6.3.x", + "@prisma/client": "6.4.x", "@trpc/client": "^11.0.0-rc.563", "@trpc/server": "^11.0.0-rc.563", "nuxt": "^3.14.1592", @@ -21,7 +21,7 @@ }, "devDependencies": { "esbuild": "^0.24.0", - "prisma": "6.3.x", + "prisma": "6.4.x", "typescript": "^5.6.2", "vue-tsc": "^2.1.10" } diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json b/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json index 2ee210852..18df0cb2f 100644 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json +++ b/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json @@ -15,7 +15,7 @@ "start": "next start" }, "dependencies": { - "@prisma/client": "6.3.x", + "@prisma/client": "6.4.x", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "^5.50.0", "@trpc/client": "^11.0.0-rc.446", @@ -39,7 +39,7 @@ "@typescript-eslint/parser": "^8.1.0", "eslint": "^8.57.0", "eslint-config-next": "^14.2.4", - "prisma": "6.3.x", + "prisma": "6.4.x", "typescript": "^5.5.3" }, "ct3aMetadata": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 545e972da..2d7149633 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -115,7 +115,7 @@ "zod-validation-error": "^1.5.0" }, "peerDependencies": { - "@prisma/client": "5.0.0 - 6.3.x" + "@prisma/client": "5.0.0 - 6.4.x" }, "author": { "name": "ZenStack Team" diff --git a/packages/schema/package.json b/packages/schema/package.json index 2c72d93e6..b7ec2df94 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -123,10 +123,10 @@ "zod-validation-error": "^1.5.0" }, "peerDependencies": { - "prisma": "5.0.0 - 6.3.x" + "prisma": "5.0.0 - 6.4.x" }, "devDependencies": { - "@prisma/client": "6.3.x", + "@prisma/client": "6.4.x", "@types/async-exit-hook": "^2.0.0", "@types/pluralize": "^0.0.29", "@types/semver": "^7.3.13", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a374bfafd..7843f0689 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -18,8 +18,8 @@ "author": "", "license": "MIT", "dependencies": { - "@prisma/generator-helper": "6.3.x", - "@prisma/internals": "6.3.x", + "@prisma/generator-helper": "6.4.x", + "@prisma/internals": "6.4.x", "@zenstackhq/language": "workspace:*", "@zenstackhq/runtime": "workspace:*", "langium": "1.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebfc51c4c..37d162db8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -392,8 +392,8 @@ importers: packages/runtime: dependencies: '@prisma/client': - specifier: 5.0.0 - 6.3.x - version: 6.3.0(prisma@6.0.0)(typescript@5.5.2) + specifier: 5.0.0 - 6.4.x + version: 6.4.1(prisma@6.0.0)(typescript@5.5.2) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -523,7 +523,7 @@ importers: specifier: ^4.0.0 version: 4.0.1 prisma: - specifier: 5.0.0 - 6.3.x + specifier: 5.0.0 - 6.4.x version: 6.0.0 semver: specifier: ^7.5.2 @@ -575,8 +575,8 @@ importers: version: 1.5.0(zod@3.23.8) devDependencies: '@prisma/client': - specifier: 6.3.x - version: 6.3.0(prisma@6.0.0)(typescript@5.5.2) + specifier: 6.4.x + version: 6.4.1(prisma@6.0.0)(typescript@5.5.2) '@types/async-exit-hook': specifier: ^2.0.0 version: 2.0.2 @@ -627,11 +627,11 @@ importers: packages/sdk: dependencies: '@prisma/generator-helper': - specifier: 6.3.x - version: 6.3.0 + specifier: 6.4.x + version: 6.4.1 '@prisma/internals': - specifier: 6.3.x - version: 6.3.0(typescript@5.5.2) + specifier: 6.4.x + version: 6.4.1(typescript@5.5.2) '@zenstackhq/language': specifier: workspace:* version: link:../language/dist @@ -2574,8 +2574,8 @@ packages: prisma: optional: true - '@prisma/client@6.3.0': - resolution: {integrity: sha512-BY3Fi28PUSk447Bpv22LhZp4HgNPo7NsEN+EteM1CLDnLjig5863jpW+3c3HHLFmml+nB/eJv1CjSriFZ8z7Cg==} + '@prisma/client@6.4.1': + resolution: {integrity: sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==} engines: {node: '>=18.18'} peerDependencies: prisma: '*' @@ -2586,14 +2586,17 @@ packages: typescript: optional: true + '@prisma/config@6.4.1': + resolution: {integrity: sha512-phJxM9V5/NJ4vFC0p1iM2Jcbe5D+Es/34BK3DVhmtCZ9NBiRskIeyp/5OY70LzgqZOsBtPhZutW2JD0+QdIVuw==} + '@prisma/debug@5.14.0': resolution: {integrity: sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==} '@prisma/debug@6.0.0': resolution: {integrity: sha512-eUjoNThlDXdyJ1iQ2d7U6aTVwm59EwvODb5zFVNJEokNoSiQmiYWNzZIwZyDmZ+j51j42/0iTaHIJ4/aZPKFRg==} - '@prisma/debug@6.3.0': - resolution: {integrity: sha512-m1lQv//0Rc5RG8TBpNUuLCxC35Ghi5XfpPmL83Gh04/GICHD2J5H2ndMlaljrUNaQDF9dOxIuFAYP1rE9wkXkg==} + '@prisma/debug@6.4.1': + resolution: {integrity: sha512-Q9xk6yjEGIThjSD8zZegxd5tBRNHYd13GOIG0nLsanbTXATiPXCLyvlYEfvbR2ft6dlRsziQXfQGxAgv7zcMUA==} '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': resolution: {integrity: sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==} @@ -2601,8 +2604,8 @@ packages: '@prisma/engines-version@5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e': resolution: {integrity: sha512-JmIds0Q2/vsOmnuTJYxY4LE+sajqjYKhLtdOT6y4imojqv5d/aeVEfbBGC74t8Be1uSp0OP8lxIj2OqoKbLsfQ==} - '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': - resolution: {integrity: sha512-R/ZcMuaWZT2UBmgX3Ko6PAV3f8//ZzsjRIG1eKqp3f2rqEqVtCv+mtzuH2rBPUC9ujJ5kCb9wwpxeyCkLcHVyA==} + '@prisma/engines-version@6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d': + resolution: {integrity: sha512-Xq54qw55vaCGrGgIJqyDwOq0TtjZPJEWsbQAHugk99hpDf2jcEeQhUcF+yzEsSqegBaDNLA4IC8Nn34sXmkiTQ==} '@prisma/engines@5.14.0': resolution: {integrity: sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==} @@ -2610,8 +2613,8 @@ packages: '@prisma/engines@6.0.0': resolution: {integrity: sha512-ZZCVP3q22ifN6Ex6C8RIcTDBlRtMJS2H1ljV0knCiWNGArvvkEbE88W3uDdq/l4+UvyvHpGzdf9ZsCWSQR7ZQQ==} - '@prisma/engines@6.3.0': - resolution: {integrity: sha512-RXqYhlZb9sx/xkUfYIZuEPn7sT0WgTxNOuEYQ7AGw3IMpP9QGVEDVsluc/GcNkM8NTJszeqk8AplJzI9lm7Jxw==} + '@prisma/engines@6.4.1': + resolution: {integrity: sha512-KldENzMHtKYwsOSLThghOIdXOBEsfDuGSrxAZjMnimBiDKd3AE4JQ+Kv+gBD/x77WoV9xIPf25GXMWffXZ17BA==} '@prisma/fetch-engine@5.14.0': resolution: {integrity: sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==} @@ -2619,14 +2622,14 @@ packages: '@prisma/fetch-engine@6.0.0': resolution: {integrity: sha512-j2m+iO5RDPRI7SUc7sHo8wX7SA4iTkJ+18Sxch8KinQM46YiCQD1iXKN6qU79C1Fliw5Bw/qDyTHaTsa3JMerA==} - '@prisma/fetch-engine@6.3.0': - resolution: {integrity: sha512-GBy0iT4f1mH31ePzfcpVSUa7JLRTeq4914FG2vR3LqDwRweSm4ja1o5flGDz+eVIa/BNYfkBvRRxv4D6ve6Eew==} + '@prisma/fetch-engine@6.4.1': + resolution: {integrity: sha512-uZ5hVeTmDspx7KcaRCNoXmcReOD+84nwlO2oFvQPRQh9xiFYnnUKDz7l9bLxp8t4+25CsaNlgrgilXKSQwrIGQ==} '@prisma/generator-helper@5.14.0': resolution: {integrity: sha512-xVc71cmTnPZ0lnSs4FAY6Ta72vFJ3webrQwKMQ2ujr6hDG1VPIEf820T1TOS3ZZQd/OKigNKXnq3co8biz9/qw==} - '@prisma/generator-helper@6.3.0': - resolution: {integrity: sha512-UhNTcfGP/mcWTih84lS4W8/qAdV6WzMDHz8r/LEmipmbpaw+6EDfa4giyGTHFSvfCXgG8HqA4S57/zDEzlGcyA==} + '@prisma/generator-helper@6.4.1': + resolution: {integrity: sha512-rf16mIHOBtgyEVfUXkqfGt94mU8Wp93yyC3iUltO7JSnnydwRNisJD/zygNCXjW7prEmKlJS4Ky5w/9z26tiLg==} '@prisma/get-platform@5.14.0': resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==} @@ -2634,14 +2637,14 @@ packages: '@prisma/get-platform@6.0.0': resolution: {integrity: sha512-PS6nYyIm9g8C03E4y7LknOfdCw/t2KyEJxntMPQHQZCOUgOpF82Ma60mdlOD08w90I3fjLiZZ0+MadenR3naDQ==} - '@prisma/get-platform@6.3.0': - resolution: {integrity: sha512-V8zZ1d0xfyi6FjpNP4AcYuwSpGcdmu35OXWnTPm8IW594PYALzKXHwIa9+o0f+Lo9AecFWrwrwaoYe56UNfTtQ==} + '@prisma/get-platform@6.4.1': + resolution: {integrity: sha512-gXqZaDI5scDkBF8oza7fOD3Q3QMD0e0rBynlzDDZdTWbWmzjuW58PRZtj+jkvKje2+ZigCWkH8SsWZAsH6q1Yw==} '@prisma/internals@5.14.0': resolution: {integrity: sha512-s0JRNDmR2bvcyy0toz89jy7SbbjANAs4e9KCReNvSm5czctIaZzDf68tcOXdtH0G7m9mKhVhNPdS9lMky0DhWA==} - '@prisma/internals@6.3.0': - resolution: {integrity: sha512-s+oOcbIRSrY8K1/GP8fL0/yMhvW8OGU6AIEcqNWpeFRC4hHPeBuQsjISc1GZREQizNXlwheVM2eb1DdaytYScw==} + '@prisma/internals@6.4.1': + resolution: {integrity: sha512-VkEhWuyFD3anHxdaCPkzgZDEIsBWojajgIhUQOBJ0+gzqAYBs1r+edJqRD6pPHnoJ7KhihM0riboZ54ZReNkDg==} peerDependencies: typescript: '>=5.1.0' peerDependenciesMeta: @@ -2654,14 +2657,14 @@ packages: '@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': resolution: {integrity: sha512-WeTmJ0mK8ALoKJUQFO+465k9lm1JWS4ODUg7akJq1wjgyDU1RTAzDFli8ESmNJlMVgJgoAd6jXmzcnoA0HT9Lg==} - '@prisma/prisma-schema-wasm@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': - resolution: {integrity: sha512-eUGf3d4K5XaQwfxr61Cbjq7ZpU+xrp5FVpt+NV+ZZQ9hxvKBL+tzi5gA4qufJ6BFC1WohgBSjelYbW+UUt3vXw==} + '@prisma/prisma-schema-wasm@6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d': + resolution: {integrity: sha512-nq1XcNXsdYbCnMFwG7QyCRLb0M4ubQrhq+7HLu7ugLJonDUy17il5qnxk9tVjkgRoEAm1QNZmqG5dXGfAN2fmQ==} '@prisma/schema-files-loader@5.14.0': resolution: {integrity: sha512-n1QHR2C63dARKPZe0WPn7biybcBHzXe+BEmiHC5Drq9KPWnpmQtIfGpqm1ZKdvCZfcA5FF3wgpSMPK4LnB0obQ==} - '@prisma/schema-files-loader@6.3.0': - resolution: {integrity: sha512-usdCjp0DWWAW6g0bOFoiSR1/qlEwd96zaVpQLimwr5tlA0CjUoBw9tX1GxilqnRtHIBNcWLmAhtVbaJfk7kYBw==} + '@prisma/schema-files-loader@6.4.1': + resolution: {integrity: sha512-6Rh9BA7nliCJ2ns8/ioEgtezsm9Sydv5vH2Ja2SgZ5c30GbY1QInmmz9jVCsm5qol0iHsnSOPSRHR2FSQW+Mmw==} '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -4648,6 +4651,11 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -10695,22 +10703,29 @@ snapshots: optionalDependencies: prisma: 6.0.0 - '@prisma/client@6.3.0(prisma@6.0.0)(typescript@5.5.2)': + '@prisma/client@6.4.1(prisma@6.0.0)(typescript@5.5.2)': optionalDependencies: prisma: 6.0.0 typescript: 5.5.2 + '@prisma/config@6.4.1': + dependencies: + esbuild: 0.24.0 + esbuild-register: 3.6.0(esbuild@0.24.0) + transitivePeerDependencies: + - supports-color + '@prisma/debug@5.14.0': {} '@prisma/debug@6.0.0': {} - '@prisma/debug@6.3.0': {} + '@prisma/debug@6.4.1': {} '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {} '@prisma/engines-version@5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e': {} - '@prisma/engines-version@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': {} + '@prisma/engines-version@6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d': {} '@prisma/engines@5.14.0': dependencies: @@ -10726,12 +10741,12 @@ snapshots: '@prisma/fetch-engine': 6.0.0 '@prisma/get-platform': 6.0.0 - '@prisma/engines@6.3.0': + '@prisma/engines@6.4.1': dependencies: - '@prisma/debug': 6.3.0 - '@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0 - '@prisma/fetch-engine': 6.3.0 - '@prisma/get-platform': 6.3.0 + '@prisma/debug': 6.4.1 + '@prisma/engines-version': 6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d + '@prisma/fetch-engine': 6.4.1 + '@prisma/get-platform': 6.4.1 '@prisma/fetch-engine@5.14.0': dependencies: @@ -10745,19 +10760,19 @@ snapshots: '@prisma/engines-version': 5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e '@prisma/get-platform': 6.0.0 - '@prisma/fetch-engine@6.3.0': + '@prisma/fetch-engine@6.4.1': dependencies: - '@prisma/debug': 6.3.0 - '@prisma/engines-version': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0 - '@prisma/get-platform': 6.3.0 + '@prisma/debug': 6.4.1 + '@prisma/engines-version': 6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d + '@prisma/get-platform': 6.4.1 '@prisma/generator-helper@5.14.0': dependencies: '@prisma/debug': 5.14.0 - '@prisma/generator-helper@6.3.0': + '@prisma/generator-helper@6.4.1': dependencies: - '@prisma/debug': 6.3.0 + '@prisma/debug': 6.4.1 '@prisma/get-platform@5.14.0': dependencies: @@ -10767,9 +10782,9 @@ snapshots: dependencies: '@prisma/debug': 6.0.0 - '@prisma/get-platform@6.3.0': + '@prisma/get-platform@6.4.1': dependencies: - '@prisma/debug': 6.3.0 + '@prisma/debug': 6.4.1 '@prisma/internals@5.14.0': dependencies: @@ -10783,34 +10798,37 @@ snapshots: arg: 5.0.2 prompts: 2.4.2 - '@prisma/internals@6.3.0(typescript@5.5.2)': + '@prisma/internals@6.4.1(typescript@5.5.2)': dependencies: - '@prisma/debug': 6.3.0 - '@prisma/engines': 6.3.0 - '@prisma/fetch-engine': 6.3.0 - '@prisma/generator-helper': 6.3.0 - '@prisma/get-platform': 6.3.0 - '@prisma/prisma-schema-wasm': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0 - '@prisma/schema-files-loader': 6.3.0 + '@prisma/config': 6.4.1 + '@prisma/debug': 6.4.1 + '@prisma/engines': 6.4.1 + '@prisma/fetch-engine': 6.4.1 + '@prisma/generator-helper': 6.4.1 + '@prisma/get-platform': 6.4.1 + '@prisma/prisma-schema-wasm': 6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d + '@prisma/schema-files-loader': 6.4.1 arg: 5.0.2 prompts: 2.4.2 optionalDependencies: typescript: 5.5.2 + transitivePeerDependencies: + - supports-color '@prisma/prisma-schema-wasm@5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85': {} '@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {} - '@prisma/prisma-schema-wasm@6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0': {} + '@prisma/prisma-schema-wasm@6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d': {} '@prisma/schema-files-loader@5.14.0': dependencies: '@prisma/prisma-schema-wasm': 5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85 fs-extra: 11.1.1 - '@prisma/schema-files-loader@6.3.0': + '@prisma/schema-files-loader@6.4.1': dependencies: - '@prisma/prisma-schema-wasm': 6.3.0-17.acc0b9dd43eb689cbd20c9470515d719db10d0b0 + '@prisma/prisma-schema-wasm': 6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d fs-extra: 11.1.1 '@protobufjs/aspromise@1.1.2': {} @@ -13138,6 +13156,13 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 + esbuild-register@3.6.0(esbuild@0.24.0): + dependencies: + debug: 4.3.5 + esbuild: 0.24.0 + transitivePeerDependencies: + - supports-color + esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts index f6b10d0ec..26fa3bc9d 100644 --- a/script/test-scaffold.ts +++ b/script/test-scaffold.ts @@ -19,6 +19,6 @@ function run(cmd: string) { } run('npm init -y'); -run('npm i --no-audit --no-fund typescript prisma@6.3.x @prisma/client@6.3.x zod@^3.22.4 decimal.js @types/node'); +run('npm i --no-audit --no-fund typescript prisma@6.4.x @prisma/client@6.4.x zod@^3.22.4 decimal.js @types/node'); console.log('Test scaffold setup complete.'); diff --git a/tests/integration/test-run/package.json b/tests/integration/test-run/package.json index 95b3e881f..50fc57c2f 100644 --- a/tests/integration/test-run/package.json +++ b/tests/integration/test-run/package.json @@ -10,9 +10,9 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "6.3.x", + "@prisma/client": "6.4.x", "@zenstackhq/runtime": "file:../../../packages/runtime/dist", - "prisma": "6.3.x", + "prisma": "6.4.x", "react": "^18.2.0", "swr": "^1.3.0", "typescript": "^4.9.3", diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 5fca21106..f5a9b2680 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -75,7 +75,7 @@ describe('CLI Plugins Tests', () => { 'swr', '@tanstack/react-query@5.56.x', '@trpc/server', - '@prisma/client@6.3.x', + '@prisma/client@6.4.x', `${path.join(__dirname, '../../../../.build/zenstackhq-language-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-sdk-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-runtime-' + ver + '.tgz')}`, @@ -85,7 +85,7 @@ describe('CLI Plugins Tests', () => { const devDepPkgs = [ 'typescript', '@types/react', - 'prisma@6.3.x', + 'prisma@6.4.x', `${path.join(__dirname, '../../../../.build/zenstack-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-tanstack-query-' + ver + '.tgz')}`, `${path.join(__dirname, '../../../../.build/zenstackhq-swr-' + ver + '.tgz')}`, diff --git a/tests/integration/tests/frameworks/nextjs/test-project/package.json b/tests/integration/tests/frameworks/nextjs/test-project/package.json index 6a29a1a57..76e8c504c 100644 --- a/tests/integration/tests/frameworks/nextjs/test-project/package.json +++ b/tests/integration/tests/frameworks/nextjs/test-project/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@prisma/client": "6.3.x", + "@prisma/client": "6.4.x", "@types/node": "18.11.18", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", @@ -26,6 +26,6 @@ "@zenstackhq/swr": "../../../../../../../packages/plugins/swr/dist" }, "devDependencies": { - "prisma": "6.3.x" + "prisma": "6.4.x" } } diff --git a/tests/integration/tests/frameworks/trpc/test-project/package.json b/tests/integration/tests/frameworks/trpc/test-project/package.json index 0882eed53..bd3d9fe83 100644 --- a/tests/integration/tests/frameworks/trpc/test-project/package.json +++ b/tests/integration/tests/frameworks/trpc/test-project/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@prisma/client": "6.3.x", + "@prisma/client": "6.4.x", "@tanstack/react-query": "^4.22.4", "@trpc/client": "^10.34.0", "@trpc/next": "^10.34.0", @@ -31,6 +31,6 @@ "@zenstackhq/trpc": "../../../../../../../packages/plugins/trpc/dist" }, "devDependencies": { - "prisma": "6.3.x" + "prisma": "6.4.x" } } From 317f5351e6ca871c2987d294d55665c91d881fdf Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 20 Feb 2025 22:07:55 -0800 Subject: [PATCH 03/13] fix(json): incorrect JSON field type generated for inputs (#1996) --- .../src/plugins/enhancer/enhance/index.ts | 58 ++++++++++++------- tests/regression/tests/issue-1991.test.ts | 48 +++++++++++++++ 2 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 tests/regression/tests/issue-1991.test.ts diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 689ddaf2c..00addd409 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -64,6 +64,12 @@ export class EnhancerGenerator { // names for models that use `auth()` in `@default` attribute private readonly modelsWithAuthInDefaultCreateInputPattern: RegExp; + // models with JSON type fields + private readonly modelsWithJsonTypeFields: DataModel[]; + + // Regex patterns for matching input/output types for models with JSON type fields + private readonly modelsWithJsonTypeFieldsInputOutputPattern: RegExp[]; + constructor( private readonly model: Model, private readonly options: PluginOptions, @@ -73,9 +79,27 @@ export class EnhancerGenerator { const modelsWithAuthInDefault = this.model.declarations.filter( (d): d is DataModel => isDataModel(d) && d.fields.some((f) => f.attributes.some(isDefaultWithAuth)) ); + this.modelsWithAuthInDefaultCreateInputPattern = new RegExp( `^(${modelsWithAuthInDefault.map((m) => m.name).join('|')})(Unchecked)?Create.*?Input$` ); + + this.modelsWithJsonTypeFields = this.model.declarations.filter( + (d): d is DataModel => isDataModel(d) && d.fields.some((f) => isTypeDef(f.type.reference?.ref)) + ); + + // input/output patterns for models with json type fields + const relevantTypePatterns = [ + 'GroupByOutputType', + '(Unchecked)?Create(\\S+?)?Input', + '(Unchecked)?Update(\\S+?)?Input', + 'CreateManyInput', + '(Unchecked)?UpdateMany(Mutation)?Input', + ]; + // build combination regex with all models with JSON types and the above suffixes + this.modelsWithJsonTypeFieldsInputOutputPattern = this.modelsWithJsonTypeFields.map( + (m) => new RegExp(`^(${m.name})(${relevantTypePatterns.join('|')})$`) + ); } async generate(): Promise<{ dmmf: DMMF.Document | undefined; newPrismaClientDtsPath: string | undefined }> { @@ -748,9 +772,6 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara } private fixJsonFieldType(typeAlias: TypeAliasDeclaration, source: string) { - const modelsWithTypeField = this.model.declarations.filter( - (d): d is DataModel => isDataModel(d) && d.fields.some((f) => isTypeDef(f.type.reference?.ref)) - ); const typeName = typeAlias.getName(); const getTypedJsonFields = (model: DataModel) => { @@ -767,7 +788,7 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara }; // fix "$[Model]Payload" type - const payloadModelMatch = modelsWithTypeField.find((m) => `$${m.name}Payload` === typeName); + const payloadModelMatch = this.modelsWithJsonTypeFields.find((m) => `$${m.name}Payload` === typeName); if (payloadModelMatch) { const scalars = typeAlias .getDescendantsOfKind(SyntaxKind.PropertySignature) @@ -783,24 +804,19 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara } // fix input/output types, "[Model]CreateInput", etc. - const inputOutputModelMatch = modelsWithTypeField.find((m) => typeName.startsWith(m.name)); - if (inputOutputModelMatch) { - const relevantTypePatterns = [ - 'GroupByOutputType', - '(Unchecked)?Create(\\S+?)?Input', - '(Unchecked)?Update(\\S+?)?Input', - 'CreateManyInput', - '(Unchecked)?UpdateMany(Mutation)?Input', - ]; - const typeRegex = modelsWithTypeField.map( - (m) => new RegExp(`^(${m.name})(${relevantTypePatterns.join('|')})$`) - ); - if (typeRegex.some((r) => r.test(typeName))) { - const fieldsToFix = getTypedJsonFields(inputOutputModelMatch); - for (const field of fieldsToFix) { - source = replacePrismaJson(source, field); - } + for (const pattern of this.modelsWithJsonTypeFieldsInputOutputPattern) { + const match = typeName.match(pattern); + if (!match) { + continue; + } + // first capture group is the model name + const modelName = match[1]; + const model = this.modelsWithJsonTypeFields.find((m) => m.name === modelName); + const fieldsToFix = getTypedJsonFields(model!); + for (const field of fieldsToFix) { + source = replacePrismaJson(source, field); } + break; } return source; diff --git a/tests/regression/tests/issue-1991.test.ts b/tests/regression/tests/issue-1991.test.ts new file mode 100644 index 000000000..da8443b6a --- /dev/null +++ b/tests/regression/tests/issue-1991.test.ts @@ -0,0 +1,48 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1991', () => { + it('regression', async () => { + await loadSchema( + ` + type FooMetadata { + isLocked Boolean + } + + type FooOptionMetadata { + color String + } + + model Foo { + id String @id @db.Uuid @default(uuid()) + meta FooMetadata @json + } + + model FooOption { + id String @id @db.Uuid @default(uuid()) + meta FooOptionMetadata @json + } + `, + { + provider: 'postgresql', + pushDb: false, + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` + import { PrismaClient } from '@prisma/client'; + import { enhance } from '.zenstack/enhance'; + + const prisma = new PrismaClient(); + const db = enhance(prisma); + + db.fooOption.create({ + data: { meta: { color: 'red' } } + }) + `, + }, + ], + } + ); + }); +}); From 0e379f81050583b69184d633796453e0f6ea2a7f Mon Sep 17 00:00:00 2001 From: Dylan Lundy <4567380+diesal11@users.noreply.github.com> Date: Sun, 23 Feb 2025 04:45:29 +1030 Subject: [PATCH 04/13] fix(schema): Provide a truncated name for unique constraints with polymorphic fk (#1999) --- .../src/plugins/prisma/schema-generator.ts | 15 ++++- tests/regression/tests/issue-1992.test.ts | 65 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 tests/regression/tests/issue-1992.test.ts diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 3c28e78a2..8df59941c 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -27,7 +27,6 @@ import { LiteralExpr, Model, NumberLiteral, - ReferenceExpr, StringLiteral, } from '@zenstackhq/language/ast'; import { getIdFields } from '@zenstackhq/sdk'; @@ -529,9 +528,15 @@ export class PrismaSchemaGenerator { if (found) { // replicate the attribute and replace the field reference with the new FK field const args: PrismaAttributeArgValue[] = []; + const fieldNames: string[] = []; for (const arg of fields.items) { - if (isReferenceExpr(arg) && arg.target.ref === origForeignKey) { + if (!isReferenceExpr(arg)) { + throw new PluginError(name, 'Unexpected field reference in @@unique attribute'); + } + + if (arg.target.ref === origForeignKey) { // replace + fieldNames.push(addedFkField.name); args.push( new PrismaAttributeArgValue( 'FieldReference', @@ -540,17 +545,21 @@ export class PrismaSchemaGenerator { ); } else { // copy + fieldNames.push(arg.target.$refText); args.push( new PrismaAttributeArgValue( 'FieldReference', - new PrismaFieldReference((arg as ReferenceExpr).target.$refText) + new PrismaFieldReference(arg.target.$refText) ) ); } } + const constraintName = this.truncate(`${dataModel.name}_${fieldNames.join('_')}_unique`); + model.addAttribute('@@unique', [ new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('Array', args)), + new PrismaAttributeArg('map', new PrismaAttributeArgValue('String', constraintName)), ]); } } diff --git a/tests/regression/tests/issue-1992.test.ts b/tests/regression/tests/issue-1992.test.ts new file mode 100644 index 000000000..51715c3a5 --- /dev/null +++ b/tests/regression/tests/issue-1992.test.ts @@ -0,0 +1,65 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1992', () => { + it('regression', async () => { + await loadSchema( + ` + enum MyAppUserType { + Local + Google + Microsoft + } + + model MyAppCompany { + id String @id @default(cuid()) + name String + users MyAppUser[] + + userFolders MyAppUserFolder[] + } + + model MyAppUser { + id String @id @default(cuid()) + companyId String + type MyAppUserType + + @@delegate(type) + + company MyAppCompany @relation(fields: [companyId], references: [id]) + userFolders MyAppUserFolder[] + } + + model MyAppUserLocal extends MyAppUser { + email String + password String + } + + model MyAppUserGoogle extends MyAppUser { + googleId String + } + + model MyAppUserMicrosoft extends MyAppUser { + microsoftId String + } + + model MyAppUserFolder { + id String @id @default(cuid()) + companyId String + userId String + path String + name String + + @@unique([companyId, userId, name]) + @@unique([companyId, userId, path]) + + company MyAppCompany @relation(fields: [companyId], references: [id]) + user MyAppUser @relation(fields: [userId], references: [id]) + } + `, + { + provider: 'postgresql', + pushDb: false, + } + ); + }); +}); From 721c938905a7c5b971e311f9f1d86e6b94dd25de Mon Sep 17 00:00:00 2001 From: Youssef Gaber <1728215+Gabrola@users.noreply.github.com> Date: Sun, 23 Feb 2025 08:56:22 +0400 Subject: [PATCH 05/13] fix: don't set default value in nested writes when set through FK (#1989) Co-authored-by: Youssef Gaber Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- .../src/enhancements/node/default-auth.ts | 114 ++++++++++----- tests/regression/tests/issue-1997.test.ts | 131 ++++++++++++++++++ 2 files changed, 209 insertions(+), 36 deletions(-) create mode 100644 tests/regression/tests/issue-1997.test.ts diff --git a/packages/runtime/src/enhancements/node/default-auth.ts b/packages/runtime/src/enhancements/node/default-auth.ts index e6162a2d2..f151d014f 100644 --- a/packages/runtime/src/enhancements/node/default-auth.ts +++ b/packages/runtime/src/enhancements/node/default-auth.ts @@ -5,10 +5,12 @@ import { ACTIONS_WITH_WRITE_PAYLOAD } from '../../constants'; import { FieldInfo, NestedWriteVisitor, + NestedWriteVisitorContext, PrismaWriteActionType, clone, enumerate, getFields, + getModelInfo, getTypeDefInfo, requireField, } from '../../cross'; @@ -61,7 +63,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) { const newArgs = clone(args); - const processCreatePayload = (model: string, data: any) => { + const processCreatePayload = (model: string, data: any, context: NestedWriteVisitorContext) => { const fields = getFields(this.options.modelMeta, model); for (const fieldInfo of Object.values(fields)) { if (fieldInfo.isTypeDef) { @@ -82,24 +84,24 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { const defaultValue = this.getDefaultValue(fieldInfo); if (defaultValue !== undefined) { // set field value extracted from `auth()` - this.setDefaultValueForModelData(fieldInfo, model, data, defaultValue); + this.setDefaultValueForModelData(fieldInfo, model, data, defaultValue, context); } } }; // visit create payload and set default value to fields using `auth()` in `@default()` const visitor = new NestedWriteVisitor(this.options.modelMeta, { - create: (model, data) => { - processCreatePayload(model, data); + create: (model, data, context) => { + processCreatePayload(model, data, context); }, - upsert: (model, data) => { - processCreatePayload(model, data.create); + upsert: (model, data, context) => { + processCreatePayload(model, data.create, context); }, - createMany: (model, args) => { + createMany: (model, args, context) => { for (const item of enumerate(args.data)) { - processCreatePayload(model, item); + processCreatePayload(model, item, context); } }, }); @@ -108,42 +110,82 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler { return newArgs; } - private setDefaultValueForModelData(fieldInfo: FieldInfo, model: string, data: any, authDefaultValue: unknown) { - if (fieldInfo.isForeignKey && fieldInfo.relationField && fieldInfo.relationField in data) { + private setDefaultValueForModelData( + fieldInfo: FieldInfo, + model: string, + data: any, + authDefaultValue: unknown, + context: NestedWriteVisitorContext + ) { + if (fieldInfo.isForeignKey) { + // if the field being inspected is a fk field, there are several cases we should not + // set the default value or should not set directly + // if the field is a fk, and the relation field is already set, we should not override it - return; - } + if (fieldInfo.relationField && fieldInfo.relationField in data) { + return; + } - if (fieldInfo.isForeignKey && !isUnsafeMutate(model, data, this.options.modelMeta)) { - // if the field is a fk, and the create payload is not unsafe, we need to translate - // the fk field setting to a `connect` of the corresponding relation field - const relFieldName = fieldInfo.relationField; - if (!relFieldName) { - throw new Error( - `Field \`${fieldInfo.name}\` is a foreign key field but no corresponding relation field is found` + if (context.field?.backLink && context.nestingPath.length > 1) { + // if the fk field is in a creation context where its implied by the parent, + // we should not set the default value, e.g.: + // + // ``` + // parent.create({ data: { child: { create: {} } } }) + // ``` + // + // event if child's fk to parent has a default value, we should not set default + // value here + + // fetch parent model from the parent context + const parentModel = getModelInfo( + this.options.modelMeta, + context.nestingPath[context.nestingPath.length - 2].model ); - } - const relationField = requireField(this.options.modelMeta, model, relFieldName); - // construct a `{ connect: { ... } }` payload - let connect = data[relationField.name]?.connect; - if (!connect) { - connect = {}; - data[relationField.name] = { connect }; + if (parentModel) { + // get the opposite side of the relation for the current create context + const oppositeRelationField = requireField(this.options.modelMeta, model, context.field.backLink); + if (parentModel.name === oppositeRelationField.type) { + // if the opposite side matches the parent model, it means we currently in a creation context + // that implicitly sets this fk field + return; + } + } } - // sets the opposite fk field to value `authDefaultValue` - const oppositeFkFieldName = this.getOppositeFkFieldName(relationField, fieldInfo); - if (!oppositeFkFieldName) { - throw new Error( - `Cannot find opposite foreign key field for \`${fieldInfo.name}\` in relation field \`${relFieldName}\`` - ); + if (!isUnsafeMutate(model, data, this.options.modelMeta)) { + // if the field is a fk, and the create payload is not unsafe, we need to translate + // the fk field setting to a `connect` of the corresponding relation field + const relFieldName = fieldInfo.relationField; + if (!relFieldName) { + throw new Error( + `Field \`${fieldInfo.name}\` is a foreign key field but no corresponding relation field is found` + ); + } + const relationField = requireField(this.options.modelMeta, model, relFieldName); + + // construct a `{ connect: { ... } }` payload + let connect = data[relationField.name]?.connect; + if (!connect) { + connect = {}; + data[relationField.name] = { connect }; + } + + // sets the opposite fk field to value `authDefaultValue` + const oppositeFkFieldName = this.getOppositeFkFieldName(relationField, fieldInfo); + if (!oppositeFkFieldName) { + throw new Error( + `Cannot find opposite foreign key field for \`${fieldInfo.name}\` in relation field \`${relFieldName}\`` + ); + } + connect[oppositeFkFieldName] = authDefaultValue; + return; } - connect[oppositeFkFieldName] = authDefaultValue; - } else { - // set default value directly - data[fieldInfo.name] = authDefaultValue; } + + // set default value directly + data[fieldInfo.name] = authDefaultValue; } private getOppositeFkFieldName(relationField: FieldInfo, fieldInfo: FieldInfo) { diff --git a/tests/regression/tests/issue-1997.test.ts b/tests/regression/tests/issue-1997.test.ts new file mode 100644 index 000000000..3153c26c6 --- /dev/null +++ b/tests/regression/tests/issue-1997.test.ts @@ -0,0 +1,131 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1997', () => { + it('regression', async () => { + const { prisma, enhance } = await loadSchema( + ` + model Tenant { + id String @id @default(uuid()) + + users User[] + posts Post[] + comments Comment[] + postUserLikes PostUserLikes[] + } + + model User { + id String @id @default(uuid()) + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + posts Post[] + likes PostUserLikes[] + + @@allow('all', true) + } + + model Post { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) + author User @relation(fields: [authorId], references: [id]) + authorId String @default(auth().id) + + comments Comment[] + likes PostUserLikes[] + + @@id([tenantId, id]) + + @@allow('all', true) + } + + model PostUserLikes { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) + + userId String + user User @relation(fields: [userId], references: [id]) + + postId String + post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) + + @@id([tenantId, id]) + @@unique([tenantId, userId, postId]) + + @@allow('all', true) + } + + model Comment { + tenantId String @default(auth().tenantId) + tenant Tenant @relation(fields: [tenantId], references: [id]) + id String @default(uuid()) + postId String + post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) + + @@id([tenantId, id]) + + @@allow('all', true) + } + `, + { logPrismaQuery: true } + ); + + const tenant = await prisma.tenant.create({ + data: {}, + }); + const user = await prisma.user.create({ + data: { tenantId: tenant.id }, + }); + + const db = enhance({ id: user.id, tenantId: tenant.id }); + + await expect( + db.post.create({ + data: { + likes: { + createMany: { + data: [ + { + userId: user.id, + }, + ], + }, + }, + }, + include: { + likes: true, + }, + }) + ).resolves.toMatchObject({ + authorId: user.id, + likes: [ + { + tenantId: tenant.id, + userId: user.id, + }, + ], + }); + + await expect( + db.post.create({ + data: { + comments: { + createMany: { + data: [{}], + }, + }, + }, + include: { + comments: true, + }, + }) + ).resolves.toMatchObject({ + authorId: user.id, + comments: [ + { + tenantId: tenant.id, + }, + ], + }); + }); +}); From d42dc32c35f57031c2dabcdd4870bffbd76932b4 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sat, 22 Feb 2025 21:50:18 -0800 Subject: [PATCH 06/13] fix(delegate): clean up generated zod schemas for delegate auxiliary fields (#2003) --- packages/schema/src/plugins/zod/generator.ts | 16 +++-- .../schema/src/plugins/zod/transformer.ts | 62 ++++++++++++++++-- packages/sdk/src/model-meta-generator.ts | 50 +-------------- packages/sdk/src/utils.ts | 51 +++++++++++++++ tests/regression/tests/issue-1993.test.ts | 63 +++++++++++++++++++ 5 files changed, 186 insertions(+), 56 deletions(-) create mode 100644 tests/regression/tests/issue-1993.test.ts diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 46e6505fe..341b8cae5 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -1,3 +1,4 @@ +import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; import { ExpressionContext, PluginError, @@ -88,12 +89,18 @@ export class ZodSchemaGenerator { (o) => !excludeModels.find((e) => e === o.model) ); - // TODO: better way of filtering than string startsWith? const inputObjectTypes = prismaClientDmmf.schema.inputObjectTypes.prisma.filter( - (type) => !excludeModels.find((e) => type.name.toLowerCase().startsWith(e.toLocaleLowerCase())) + (type) => + !excludeModels.some((e) => type.name.toLowerCase().startsWith(e.toLocaleLowerCase())) && + // exclude delegate aux related types + !type.name.toLowerCase().includes(DELEGATE_AUX_RELATION_PREFIX) ); + const outputObjectTypes = prismaClientDmmf.schema.outputObjectTypes.prisma.filter( - (type) => !excludeModels.find((e) => type.name.toLowerCase().startsWith(e.toLowerCase())) + (type) => + !excludeModels.some((e) => type.name.toLowerCase().startsWith(e.toLowerCase())) && + // exclude delegate aux related types + !type.name.toLowerCase().includes(DELEGATE_AUX_RELATION_PREFIX) ); const models: DMMF.Model[] = prismaClientDmmf.datamodel.models.filter( @@ -236,7 +243,8 @@ export class ZodSchemaGenerator { const moduleNames: string[] = []; for (let i = 0; i < inputObjectTypes.length; i += 1) { - const fields = inputObjectTypes[i]?.fields; + // exclude delegate aux fields + const fields = inputObjectTypes[i]?.fields?.filter((f) => !f.name.startsWith(DELEGATE_AUX_RELATION_PREFIX)); const name = inputObjectTypes[i]?.name; if (!generateUnchecked && name.includes('Unchecked')) { diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index db0c2a7bb..8e7364669 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,8 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; import { getForeignKeyFields, + getRelationBackLink, hasAttribute, indentString, + isDelegateModel, isDiscriminatorField, type PluginOptions, } from '@zenstackhq/sdk'; @@ -67,7 +70,11 @@ export default class Transformer { const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`); const content = `${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( `${name}`, - `z.enum(${JSON.stringify(enumType.values)})` + `z.enum(${JSON.stringify( + enumType.values + // exclude fields generated for delegate models + .filter((v) => !v.startsWith(DELEGATE_AUX_RELATION_PREFIX)) + )})` )}`; this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); generated.push(enumType.name); @@ -243,12 +250,19 @@ export default class Transformer { !isFieldRef && (inputType.namespace === 'prisma' || isEnum) ) { - if (inputType.type !== this.originalName && typeof inputType.type === 'string') { - this.addSchemaImport(inputType.type); + // reduce concrete input types to their delegate base types + // e.g.: "UserCreateNestedOneWithoutDelegate_aux_PostInput" => "UserCreateWithoutAssetInput" + let mappedInputType = inputType; + if (contextDataModel) { + mappedInputType = this.mapDelegateInputType(inputType, contextDataModel, field.name); + } + + if (mappedInputType.type !== this.originalName && typeof mappedInputType.type === 'string') { + this.addSchemaImport(mappedInputType.type); } const contextField = contextDataModel?.fields.find((f) => f.name === field.name); - result.push(this.generatePrismaStringLine(field, inputType, lines.length, contextField)); + result.push(this.generatePrismaStringLine(field, mappedInputType, lines.length, contextField)); } } @@ -289,6 +303,46 @@ export default class Transformer { return [[` ${fieldName} ${resString} `, field, true]]; } + private mapDelegateInputType( + inputType: PrismaDMMF.InputTypeRef, + contextDataModel: DataModel, + contextFieldName: string + ) { + // input type mapping is only relevant for relation inherited from delegate models + const contextField = contextDataModel.fields.find((f) => f.name === contextFieldName); + if (!contextField || !isDataModel(contextField.type.reference?.ref)) { + return inputType; + } + + if (!contextField.$inheritedFrom || !isDelegateModel(contextField.$inheritedFrom)) { + return inputType; + } + + let processedInputType = inputType; + + // captures: model name and operation, "Without" part that references a concrete model, + // and the "Input" or "NestedInput" suffix + const match = inputType.type.match(/^(\S+?)((NestedOne)?WithoutDelegate_aux\S+?)((Nested)?Input)$/); + if (match) { + let mappedInputTypeName = match[1]; + + if (contextDataModel) { + // get the opposite side of the relation field, which should be of the proper + // delegate base type + const oppositeRelationField = getRelationBackLink(contextField); + if (oppositeRelationField) { + mappedInputTypeName += `Without${upperCaseFirst(oppositeRelationField.name)}`; + } + } + + // "Input" or "NestedInput" suffix + mappedInputTypeName += match[4]; + + processedInputType = { ...inputType, type: mappedInputTypeName }; + } + return processedInputType; + } + wrapWithZodValidators( mainValidators: string | string[], field: PrismaDMMF.SchemaArg, diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts index 71b4246ca..88e064512 100644 --- a/packages/sdk/src/model-meta-generator.ts +++ b/packages/sdk/src/model-meta-generator.ts @@ -24,16 +24,15 @@ import { ExpressionContext, getAttribute, getAttributeArg, - getAttributeArgLiteral, getAttributeArgs, getAuthDecl, getDataModels, getInheritedFromDelegate, getLiteral, + getRelationBackLink, getRelationField, hasAttribute, isAuthInvocation, - isDelegateModel, isEnumFieldReference, isForeignKeyField, isIdField, @@ -289,7 +288,7 @@ function writeFields( if (dmField) { // metadata specific to DataModelField - const backlink = getBackLink(dmField); + const backlink = getRelationBackLink(dmField); const fkMapping = generateForeignKeyMapping(dmField); if (backlink) { @@ -336,51 +335,6 @@ function writeFields( writer.write(','); } -function getBackLink(field: DataModelField) { - if (!field.type.reference?.ref || !isDataModel(field.type.reference?.ref)) { - return undefined; - } - - const relName = getRelationName(field); - - let sourceModel: DataModel; - if (field.$inheritedFrom && isDelegateModel(field.$inheritedFrom)) { - // field is inherited from a delegate model, use it as the source - sourceModel = field.$inheritedFrom; - } else { - // otherwise use the field's container model as the source - sourceModel = field.$container as DataModel; - } - - const targetModel = field.type.reference.ref as DataModel; - - for (const otherField of targetModel.fields) { - if (otherField === field) { - // backlink field is never self - continue; - } - if (otherField.type.reference?.ref === sourceModel) { - if (relName) { - const otherRelName = getRelationName(otherField); - if (relName === otherRelName) { - return otherField; - } - } else { - return otherField; - } - } - } - return undefined; -} - -function getRelationName(field: DataModelField) { - const relAttr = getAttribute(field, '@relation'); - if (!relAttr) { - return undefined; - } - return getAttributeArgLiteral(relAttr, 'name'); -} - function getAttributes(target: DataModelField | DataModel | TypeDefField): RuntimeAttribute[] { return target.attributes .map((attr) => { diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index ecb6895eb..93118b5f5 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -632,3 +632,54 @@ export function getInheritanceChain(from: DataModel, to: DataModel): DataModel[] return undefined; } + +/** + * Get the opposite side of a relation field. + */ +export function getRelationBackLink(field: DataModelField) { + if (!field.type.reference?.ref || !isDataModel(field.type.reference?.ref)) { + return undefined; + } + + const relName = getRelationName(field); + + let sourceModel: DataModel; + if (field.$inheritedFrom && isDelegateModel(field.$inheritedFrom)) { + // field is inherited from a delegate model, use it as the source + sourceModel = field.$inheritedFrom; + } else { + // otherwise use the field's container model as the source + sourceModel = field.$container as DataModel; + } + + const targetModel = field.type.reference.ref as DataModel; + + for (const otherField of targetModel.fields) { + if (otherField === field) { + // backlink field is never self + continue; + } + if (otherField.type.reference?.ref === sourceModel) { + if (relName) { + const otherRelName = getRelationName(otherField); + if (relName === otherRelName) { + return otherField; + } + } else { + return otherField; + } + } + } + return undefined; +} + +/** + * Get the relation name of a relation field. + */ +export function getRelationName(field: DataModelField) { + const relAttr = getAttribute(field, '@relation'); + if (!relAttr) { + return undefined; + } + return getAttributeArgLiteral(relAttr, 'name'); +} diff --git a/tests/regression/tests/issue-1993.test.ts b/tests/regression/tests/issue-1993.test.ts new file mode 100644 index 000000000..23561f8e4 --- /dev/null +++ b/tests/regression/tests/issue-1993.test.ts @@ -0,0 +1,63 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1993', () => { + it('regression', async () => { + const { zodSchemas } = await loadSchema( + ` +enum UserType { + UserLocal + UserGoogle +} + +model User { + id String @id @default(cuid()) + companyId String? + type UserType + + @@delegate(type) + + userFolders UserFolder[] + + @@allow('all', true) +} + +model UserLocal extends User { + email String + password String +} + +model UserGoogle extends User { + googleId String +} + +model UserFolder { + id String @id @default(cuid()) + userId String + path String + + user User @relation(fields: [userId], references: [id]) + + @@allow('all', true) +} `, + { pushDb: false, fullZod: true, compile: true, output: 'lib/zenstack' } + ); + + expect( + zodSchemas.input.UserLocalInputSchema.create.safeParse({ + data: { + email: 'test@example.com', + password: 'password', + }, + }) + ).toMatchObject({ success: true }); + + expect( + zodSchemas.input.UserFolderInputSchema.create.safeParse({ + data: { + path: '/', + userId: '1', + }, + }) + ).toMatchObject({ success: true }); + }); +}); From 54a9b21c11e25cf07c89cb95cab5588f758b6bb3 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 23 Feb 2025 15:50:43 -0800 Subject: [PATCH 07/13] fix(delegate): deleteMany fails when the model has compound id fields (#2004) --- .../runtime/src/enhancements/node/delegate.ts | 21 ++++++- tests/regression/tests/issue-1998.test.ts | 59 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/regression/tests/issue-1998.test.ts diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts index 06c1526e5..59bc79793 100644 --- a/packages/runtime/src/enhancements/node/delegate.ts +++ b/packages/runtime/src/enhancements/node/delegate.ts @@ -1106,7 +1106,18 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { const entities = await db[model].findMany(findArgs); // recursively delete base entities (they all have the same id values) - await Promise.all(entities.map((entity) => this.doDelete(db, model, { where: entity }))); + + await Promise.all( + entities.map((entity) => { + let deleteFilter = entity; + if (Object.keys(deleteFilter).length > 1) { + // if the model has compound id fields, we need to compose a compound key filter, + // otherwise calling Prisma's `delete` won't work + deleteFilter = this.queryUtils.composeCompoundUniqueField(model, deleteFilter); + } + return this.doDelete(db, model, { where: deleteFilter }); + }) + ); return { count: entities.length }; } @@ -1114,7 +1125,13 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { private async deleteBaseRecursively(db: CrudContract, model: string, idValues: any) { let base = this.getBaseModel(model); while (base) { - await db[base.name].delete({ where: idValues }); + let deleteFilter = idValues; + if (Object.keys(idValues).length > 1) { + // if the model has compound id fields, we need to compose a compound key filter, + // otherwise calling Prisma's `delete` won't work + deleteFilter = this.queryUtils.composeCompoundUniqueField(base.name, deleteFilter); + } + await db[base.name].delete({ where: deleteFilter }); base = this.getBaseModel(base.name); } } diff --git a/tests/regression/tests/issue-1998.test.ts b/tests/regression/tests/issue-1998.test.ts new file mode 100644 index 000000000..a2810dfea --- /dev/null +++ b/tests/regression/tests/issue-1998.test.ts @@ -0,0 +1,59 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1998', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model Entity { + id String @id + type String + updatable Boolean + children Relation[] @relation("children") + parents Relation[] @relation("parents") + + @@delegate(type) + @@allow('create,read', true) + @@allow('update', updatable) + } + + model A extends Entity {} + + model B extends Entity {} + + model Relation { + parent Entity @relation("children", fields: [parentId], references: [id]) + parentId String + child Entity @relation("parents", fields: [childId], references: [id]) + childId String + + @@allow('create', true) + @@allow('read', check(parent, 'read') && check(child, 'read')) + @@allow('delete', check(parent, 'update') && check(child, 'update')) + + @@id([parentId, childId]) + } + ` + ); + + const db = enhance(); + + await db.a.create({ data: { id: '1', updatable: true } }); + await db.b.create({ data: { id: '2', updatable: true } }); + await db.relation.create({ data: { parentId: '1', childId: '2' } }); + + await expect( + db.relation.deleteMany({ + where: { parentId: '1', childId: '2' }, + }) + ).resolves.toEqual({ count: 1 }); + + await db.a.create({ data: { id: '3', updatable: false } }); + await db.b.create({ data: { id: '4', updatable: false } }); + await db.relation.create({ data: { parentId: '3', childId: '4' } }); + await expect( + db.relation.deleteMany({ + where: { parentId: '3', childId: '4' }, + }) + ).resolves.toEqual({ count: 0 }); + }); +}); From f0f6f3f2c1792bf7efde23fd1dfe0c532efe7929 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 23 Feb 2025 16:44:53 -0800 Subject: [PATCH 08/13] fix(zmodel): validate `@regex` regular expression patterns (#2006) --- .../attribute-application-validator.ts | 15 +++++++++++++ .../validation/attribute-validation.test.ts | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index 0e1d8e885..b22a6f372 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -212,6 +212,21 @@ export default class AttributeApplicationValidator implements AstValidator { if (isDataModelFieldReference(node) && hasAttribute(node.target.ref as DataModelField, '@encrypted')) { diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 0133f452c..3e0553ee2 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -1369,4 +1369,26 @@ describe('Attribute tests', () => { `) ).resolves.toContain(`attribute "@omit" cannot be used on type declaration fields`); }); + + it('validates regex', async () => { + await expect( + loadModelWithError(` + ${prelude} + model User { + id String @id + phone String @regex(id) + } + `) + ).resolves.toContain('Expecting a string literal'); + + await expect( + loadModelWithError(` + ${prelude} + model User { + id String @id + phone String @regex("^(\\+46|0)[0-9]{7,12}$") + } + `) + ).resolves.toContain('Invalid regular expression'); + }); }); From 7cdf04b9e89d3c7013feca961eb73bbcfb25b449 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 23 Feb 2025 16:45:13 -0800 Subject: [PATCH 09/13] fix(policy): update fails for model using both `@password` and `@@validate` (#2005) --- .../enhancements/node/policy/policy-utils.ts | 72 ++++++++++++++----- tests/regression/tests/issue-2000.test.ts | 67 +++++++++++++++++ 2 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 tests/regression/tests/issue-2000.test.ts diff --git a/packages/runtime/src/enhancements/node/policy/policy-utils.ts b/packages/runtime/src/enhancements/node/policy/policy-utils.ts index 82a5bc88e..94c1f7f20 100644 --- a/packages/runtime/src/enhancements/node/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/node/policy/policy-utils.ts @@ -826,6 +826,8 @@ export class PolicyUtil extends QueryUtils { /** * Given a model and a unique filter, checks the operation is allowed by policies and field validations. * Rejects with an error if not allowed. + * + * This method is only called by mutation operations. */ async checkPolicyForUnique( model: string, @@ -1365,32 +1367,68 @@ export class PolicyUtil extends QueryUtils { excludePasswordFields: boolean = true, kind: 'create' | 'update' | undefined = undefined ) { + if (!this.zodSchemas) { + return undefined; + } + if (!this.hasFieldValidation(model)) { return undefined; } + const schemaKey = `${upperCaseFirst(model)}${kind ? 'Prisma' + upperCaseFirst(kind) : ''}Schema`; - let result = this.zodSchemas?.models?.[schemaKey] as ZodObject | undefined; - - if (result && excludePasswordFields) { - // fields with `@password` attribute changes at runtime, so we cannot directly use the generated - // zod schema to validate it, instead, the validation happens when checking the input of "create" - // and "update" operations - const modelFields = this.modelMeta.models[lowerCaseFirst(model)]?.fields; - if (modelFields) { - for (const [key, field] of Object.entries(modelFields)) { - if (field.attributes?.some((attr) => attr.name === '@password')) { - // override `@password` field schema with a string schema - let pwFieldSchema: ZodSchema = z.string(); - if (field.isOptional) { - pwFieldSchema = pwFieldSchema.nullish(); + + if (excludePasswordFields) { + // The `excludePasswordFields` mode is to handle the issue the fields marked with `@password` change at runtime, + // so they can only be fully validated when processing the input of "create" and "update" operations. + // + // When excluding them, we need to override them with plain string schemas. However, since the scheme is not always + // an `ZodObject` (this happens when there's `@@validate` refinement), we need to fetch the `ZodObject` schema before + // the refinement is applied, override the `@password` fields and then re-apply the refinement. + + let schema: ZodObject | undefined; + + const overridePasswordFields = (schema: z.ZodObject) => { + let result = schema; + const modelFields = this.modelMeta.models[lowerCaseFirst(model)]?.fields; + if (modelFields) { + for (const [key, field] of Object.entries(modelFields)) { + if (field.attributes?.some((attr) => attr.name === '@password')) { + // override `@password` field schema with a string schema + let pwFieldSchema: ZodSchema = z.string(); + if (field.isOptional) { + pwFieldSchema = pwFieldSchema.nullish(); + } + result = result.merge(z.object({ [key]: pwFieldSchema })); } - result = result?.merge(z.object({ [key]: pwFieldSchema })); } } + return result; + }; + + // get the schema without refinement: `[Model]WithoutRefineSchema` + const withoutRefineSchemaKey = `${upperCaseFirst(model)}${ + kind ? 'Prisma' + upperCaseFirst(kind) : '' + }WithoutRefineSchema`; + schema = this.zodSchemas.models[withoutRefineSchemaKey] as ZodObject | undefined; + + if (schema) { + // the schema has refinement, need to call refine function after schema merge + schema = overridePasswordFields(schema); + // refine function: `refine[Model]` + const refineFuncKey = `refine${upperCaseFirst(model)}`; + const refineFunc = this.zodSchemas.models[refineFuncKey] as unknown as ( + schema: ZodObject + ) => ZodSchema; + return typeof refineFunc === 'function' ? refineFunc(schema) : schema; + } else { + // otherwise, directly override the `@password` fields + schema = this.zodSchemas.models[schemaKey] as ZodObject | undefined; + return schema ? overridePasswordFields(schema) : undefined; } + } else { + // simply return the schema + return this.zodSchemas.models[schemaKey]; } - - return result; } /** diff --git a/tests/regression/tests/issue-2000.test.ts b/tests/regression/tests/issue-2000.test.ts new file mode 100644 index 000000000..8f8e50b57 --- /dev/null +++ b/tests/regression/tests/issue-2000.test.ts @@ -0,0 +1,67 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 2000', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + abstract model Base { + id String @id @default(uuid()) @deny('update', true) + createdAt DateTime @default(now()) @deny('update', true) + updatedAt DateTime @updatedAt @deny('update', true) + active Boolean @default(false) + published Boolean @default(true) + deleted Boolean @default(false) + startDate DateTime? + endDate DateTime? + + @@allow('create', true) + @@allow('read', true) + @@allow('update', true) + } + + enum EntityType { + User + Alias + Group + Service + Device + Organization + Guest + } + + model Entity extends Base { + entityType EntityType + name String? @unique + members Entity[] @relation("members") + memberOf Entity[] @relation("members") + @@delegate(entityType) + + + @@allow('create', true) + @@allow('read', true) + @@allow('update', true) + @@validate(!active || (active && name != null), "Active Entities Must Have A Name") + } + + model User extends Entity { + profile Json? + username String @unique + password String @password + + @@allow('create', true) + @@allow('read', true) + @@allow('update', true) + } + ` + ); + + const db = enhance(); + await expect(db.user.create({ data: { username: 'admin', password: 'abc12345' } })).toResolveTruthy(); + await expect( + db.user.update({ where: { username: 'admin' }, data: { password: 'abc123456789123' } }) + ).toResolveTruthy(); + + // violating validation rules + await expect(db.user.update({ where: { username: 'admin' }, data: { active: true } })).toBeRejectedByPolicy(); + }); +}); From be57ad1c0316b24569acfbfd01247d0ff887e857 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 23 Feb 2025 22:27:46 -0800 Subject: [PATCH 10/13] chore: bump version (#2008) --- package.json | 2 +- packages/ide/jetbrains/build.gradle.kts | 2 +- packages/ide/jetbrains/package.json | 2 +- packages/language/package.json | 2 +- packages/misc/redwood/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index cd08cf7ad..e76375caf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "2.11.6", + "version": "2.12.0", "description": "", "scripts": { "build": "pnpm -r --filter=\"!./packages/ide/*\" build", diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index b6dedac5b..ea42d3c2c 100644 --- a/packages/ide/jetbrains/build.gradle.kts +++ b/packages/ide/jetbrains/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "dev.zenstack" -version = "2.11.6" +version = "2.12.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index b8c745cb0..1756b93c7 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "2.11.6", + "version": "2.12.0", "displayName": "ZenStack JetBrains IDE Plugin", "description": "ZenStack JetBrains IDE plugin", "homepage": "https://zenstack.dev", diff --git a/packages/language/package.json b/packages/language/package.json index 5ca53d403..cb604c634 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "2.11.6", + "version": "2.12.0", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json index 91f1120b9..185fc341f 100644 --- a/packages/misc/redwood/package.json +++ b/packages/misc/redwood/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/redwood", "displayName": "ZenStack RedwoodJS Integration", - "version": "2.11.6", + "version": "2.12.0", "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", "repository": { "type": "git", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index eab38e311..18f44c96a 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "2.11.6", + "version": "2.12.0", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index d26596d20..470848279 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "2.11.6", + "version": "2.12.0", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index d23dc40ff..d1f4b5852 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "2.11.6", + "version": "2.12.0", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index cd581c42a..88accf0d6 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "2.11.6", + "version": "2.12.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 2d7149633..894ca7541 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "2.11.6", + "version": "2.12.0", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index b7ec2df94..98c337e03 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI", - "version": "2.11.6", + "version": "2.12.0", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 7843f0689..46f969299 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "2.11.6", + "version": "2.12.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index f3b5db083..46720f463 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "2.11.6", + "version": "2.12.0", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 9456abb9c..51533ab1f 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "2.11.6", + "version": "2.12.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From 767d59d6480f868d74587236aeb8bef5b11e3599 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Mon, 24 Feb 2025 11:02:10 -0800 Subject: [PATCH 11/13] feat: export `Enhanced` type to infer typeof enhanced PrismaClient (#2010) --- packages/runtime/res/enhance.d.ts | 2 +- packages/runtime/src/enhance.d.ts | 2 +- .../src/plugins/enhancer/enhance/index.ts | 13 ++++ .../typing/enhancement-typing.test.ts | 60 +++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/integration/tests/enhancements/typing/enhancement-typing.test.ts diff --git a/packages/runtime/res/enhance.d.ts b/packages/runtime/res/enhance.d.ts index e6ca800d2..3058f736e 100644 --- a/packages/runtime/res/enhance.d.ts +++ b/packages/runtime/res/enhance.d.ts @@ -1 +1 @@ -export { auth, enhance, type PrismaClient } from '.zenstack/enhance'; +export { auth, enhance, type PrismaClient, type Enhanced } from '.zenstack/enhance'; diff --git a/packages/runtime/src/enhance.d.ts b/packages/runtime/src/enhance.d.ts index 38a519830..9a4fe97cd 100644 --- a/packages/runtime/src/enhance.d.ts +++ b/packages/runtime/src/enhance.d.ts @@ -1,2 +1,2 @@ // @ts-expect-error stub for re-exporting generated code -export { auth, enhance } from '.zenstack/enhance'; +export { auth, enhance, type PrismaClient, type Enhanced } from '.zenstack/enhance'; diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 00addd409..1cd49c0a4 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -266,6 +266,19 @@ export function enhance(prisma: any, context?: EnhancementContext<${authTypePara ...options }, context); } + +/** + * Infers the type of PrismaClient with ZenStack's enhancements. + * @example + * type EnhancedPrismaClient = Enhanced; + */ +export type Enhanced = + Client extends _PrismaClient ? PrismaClient : + Client extends DynamicClientExtensionThis ? DynamicClientExtensionThis, Prisma.TypeMapCb, ExtArgs${ + hasClientOptions ? ', ClientOptions' : '' + }> : Client; `; } diff --git a/tests/integration/tests/enhancements/typing/enhancement-typing.test.ts b/tests/integration/tests/enhancements/typing/enhancement-typing.test.ts new file mode 100644 index 000000000..d29897a71 --- /dev/null +++ b/tests/integration/tests/enhancements/typing/enhancement-typing.test.ts @@ -0,0 +1,60 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Enhancement typing tests', () => { + it('infers correct typing', async () => { + await loadSchema( + ` + model User { + id Int @id @default(autoincrement()) + posts Post[] + } + + model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id]) + authorId Int @default(auth().id) + } + `, + { + pushDb: false, + compile: true, + extraSourceFiles: [ + { + name: 'main.ts', + content: ` +import { PrismaClient } from '@prisma/client'; +import type { Enhanced } from '.zenstack/enhance'; + +async function withoutClientExtension() { + const prisma = new PrismaClient(); + const db = {} as any as Enhanced; + // note that "author" becomes optional + const r = await db.post.create({ data: { title: 'Post1' }}); + console.log(r); +} + +async function withClientExtension() { + const prisma = (new PrismaClient()) + .$extends({ + client: { + $log: (message: string) => { + console.log(message); + }, + }, + }); + const db = {} as any as Enhanced; + // note that "author" becomes optional + const r = await db.post.create({ data: { title: 'Post1' }}); + console.log(r); + + // note that "$log" is preserved + db.$log('hello'); +} + `, + }, + ], + } + ); + }); +}); From 8e93a490a2358e2904270540be95f6e2c954dd05 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Mon, 24 Feb 2025 14:01:31 -0800 Subject: [PATCH 12/13] fix(policy): incorrect field-level permission check for nested create (#2011) --- .../src/enhancements/node/policy/handler.ts | 64 +++++++------ .../enhancements/node/policy/policy-utils.ts | 20 ++-- .../src/enhancements/node/query-utils.ts | 12 +++ tests/regression/tests/issue-2007.test.ts | 93 +++++++++++++++++++ 4 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 tests/regression/tests/issue-2007.test.ts diff --git a/packages/runtime/src/enhancements/node/policy/handler.ts b/packages/runtime/src/enhancements/node/policy/handler.ts index 673665dd5..5c5fdd4ca 100644 --- a/packages/runtime/src/enhancements/node/policy/handler.ts +++ b/packages/runtime/src/enhancements/node/policy/handler.ts @@ -284,9 +284,16 @@ export class PolicyProxyHandler implements Pr if (context.field?.backLink) { const backLinkField = resolveField(this.modelMeta, model, context.field.backLink); if (backLinkField?.isRelationOwner) { - // the target side of relation owns the relation, - // check if it's updatable - await this.policyUtils.checkPolicyForUnique(model, args.where, 'update', db, args); + // "connect" is actually "update" to foreign keys, so we need to map the "connect" payload + // to "update" payload by translating pk to fks, and use that to check update policies + const fieldsToUpdate = Object.values(backLinkField.foreignKeyMapping ?? {}); + await this.policyUtils.checkPolicyForUnique( + model, + args.where, + 'update', + db, + fieldsToUpdate + ); } } @@ -319,9 +326,12 @@ export class PolicyProxyHandler implements Pr // check existence await this.policyUtils.checkExistence(db, model, args, true); - // the target side of relation owns the relation, - // check if it's updatable - await this.policyUtils.checkPolicyForUnique(model, args, 'update', db, args); + // the target side of relation owns the relation, check if it's updatable + + // "connect" is actually "update" to foreign keys, so we need to map the "connect" payload + // to "update" payload by translating pk to fks, and use that to check update policies + const fieldsToUpdate = Object.values(backLinkField.foreignKeyMapping ?? {}); + await this.policyUtils.checkPolicyForUnique(model, args, 'update', db, fieldsToUpdate); } } }, @@ -909,21 +919,11 @@ export class PolicyProxyHandler implements Pr } // update happens on the related model, require updatable, - // translate args to foreign keys so field-level policies can be checked - const checkArgs: any = {}; - if (args && typeof args === 'object' && backLinkField.foreignKeyMapping) { - for (const key of Object.keys(args)) { - const fk = backLinkField.foreignKeyMapping[key]; - if (fk) { - checkArgs[fk] = args[key]; - } - } - } - // `uniqueFilter` can be undefined if the entity to be disconnected doesn't exist if (uniqueFilter) { - // check for update - await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, checkArgs); + // check for update, "connect" and "disconnect" are actually "update" to foreign keys + const fieldsToUpdate = Object.values(backLinkField.foreignKeyMapping ?? {}); + await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, fieldsToUpdate); // register post-update check await _registerPostUpdateCheck(model, uniqueFilter, uniqueFilter); @@ -971,12 +971,18 @@ export class PolicyProxyHandler implements Pr this.policyUtils.tryReject(db, this.model, 'update'); // check pre-update guard - await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args); + await this.policyUtils.checkPolicyForUnique( + model, + uniqueFilter, + 'update', + db, + this.queryUtils.getFieldsWithDefinedValues(updatePayload) + ); // handle the case where id fields are updated const _args: any = args; - const updatePayload = _args.data && typeof _args.data === 'object' ? _args.data : _args; - const postUpdateIds = this.calculatePostUpdateIds(model, existing, updatePayload); + const checkPayload = _args.data && typeof _args.data === 'object' ? _args.data : _args; + const postUpdateIds = this.calculatePostUpdateIds(model, existing, checkPayload); // register post-update check await _registerPostUpdateCheck(model, existing, postUpdateIds); @@ -1068,7 +1074,13 @@ export class PolicyProxyHandler implements Pr // update case // check pre-update guard - await this.policyUtils.checkPolicyForUnique(model, existing, 'update', db, args); + await this.policyUtils.checkPolicyForUnique( + model, + existing, + 'update', + db, + this.queryUtils.getFieldsWithDefinedValues(args.update) + ); // handle the case where id fields are updated const postUpdateIds = this.calculatePostUpdateIds(model, existing, args.update); @@ -1156,7 +1168,7 @@ export class PolicyProxyHandler implements Pr await this.policyUtils.checkExistence(db, model, uniqueFilter, true); // check delete guard - await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'delete', db, args); + await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'delete', db, []); }, deleteMany: async (model, args, context) => { @@ -1526,7 +1538,7 @@ export class PolicyProxyHandler implements Pr await this.policyUtils.checkExistence(tx, this.model, args.where, true); // inject delete guard - await this.policyUtils.checkPolicyForUnique(this.model, args.where, 'delete', tx, args); + await this.policyUtils.checkPolicyForUnique(this.model, args.where, 'delete', tx, []); // proceed with the deletion if (this.shouldLogQuery) { @@ -1773,7 +1785,7 @@ export class PolicyProxyHandler implements Pr private async runPostWriteChecks(postWriteChecks: PostWriteCheckRecord[], db: CrudContract) { await Promise.all( postWriteChecks.map(async ({ model, operation, uniqueFilter, preValue }) => - this.policyUtils.checkPolicyForUnique(model, uniqueFilter, operation, db, undefined, preValue) + this.policyUtils.checkPolicyForUnique(model, uniqueFilter, operation, db, [], preValue) ) ); } diff --git a/packages/runtime/src/enhancements/node/policy/policy-utils.ts b/packages/runtime/src/enhancements/node/policy/policy-utils.ts index 94c1f7f20..ef4285d78 100644 --- a/packages/runtime/src/enhancements/node/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/node/policy/policy-utils.ts @@ -451,7 +451,11 @@ export class PolicyUtil extends QueryUtils { if (operation === 'update' && args) { // merge field-level policy guards - const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args); + const fieldUpdateGuard = this.getFieldUpdateGuards( + db, + model, + this.getFieldsWithDefinedValues(args.data ?? args) + ); if (fieldUpdateGuard.rejectedByField) { // rejected args.where = this.makeFalse(); @@ -834,7 +838,7 @@ export class PolicyUtil extends QueryUtils { uniqueFilter: any, operation: PolicyOperationKind, db: CrudContract, - args: any, + fieldsToUpdate: string[], preValue?: any ) { let guard = this.getAuthGuard(db, model, operation, preValue); @@ -849,9 +853,9 @@ export class PolicyUtil extends QueryUtils { let entityChecker: EntityChecker | undefined; - if (operation === 'update' && args) { + if (operation === 'update' && fieldsToUpdate.length > 0) { // merge field-level policy guards - const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args); + const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, fieldsToUpdate); if (fieldUpdateGuard.rejectedByField) { // rejected throw this.deniedByPolicy( @@ -989,16 +993,12 @@ export class PolicyUtil extends QueryUtils { return this.and(...allFieldGuards); } - private getFieldUpdateGuards(db: CrudContract, model: string, args: any) { + private getFieldUpdateGuards(db: CrudContract, model: string, fieldsToUpdate: string[]) { const allFieldGuards = []; const allOverrideFieldGuards = []; let entityChecker: EntityChecker | undefined; - for (const [field, value] of Object.entries(args.data ?? args)) { - if (typeof value === 'undefined') { - continue; - } - + for (const field of fieldsToUpdate) { const fieldInfo = resolveField(this.modelMeta, model, field); if (fieldInfo?.isDataModel) { diff --git a/packages/runtime/src/enhancements/node/query-utils.ts b/packages/runtime/src/enhancements/node/query-utils.ts index c09fe1f95..75e729b0f 100644 --- a/packages/runtime/src/enhancements/node/query-utils.ts +++ b/packages/runtime/src/enhancements/node/query-utils.ts @@ -253,4 +253,16 @@ export class QueryUtils { return undefined; } + + /** + * Gets fields of object with defined values. + */ + getFieldsWithDefinedValues(data: object) { + if (!data) { + return []; + } + return Object.entries(data) + .filter(([, v]) => v !== undefined) + .map(([k]) => k); + } } diff --git a/tests/regression/tests/issue-2007.test.ts b/tests/regression/tests/issue-2007.test.ts new file mode 100644 index 000000000..4a4b9cbe6 --- /dev/null +++ b/tests/regression/tests/issue-2007.test.ts @@ -0,0 +1,93 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 2007', () => { + it('regression1', async () => { + const { enhance } = await loadSchema( + ` + model Page { + id String @id @default(cuid()) + title String + + images Image[] + + @@allow('all', true) + } + + model Image { + id String @id @default(cuid()) @deny('update', true) + url String + pageId String? + page Page? @relation(fields: [pageId], references: [id]) + + @@allow('all', true) + } + ` + ); + + const db = enhance(); + + const image = await db.image.create({ + data: { + url: 'https://example.com/image.png', + }, + }); + + await expect( + db.image.update({ + where: { id: image.id }, + data: { + page: { + create: { + title: 'Page 1', + }, + }, + }, + }) + ).toResolveTruthy(); + }); + + it('regression2', async () => { + const { enhance } = await loadSchema( + ` + model Page { + id String @id @default(cuid()) + title String + + images Image[] + + @@allow('all', true) + } + + model Image { + id String @id @default(cuid()) + url String + pageId String? @deny('update', true) + page Page? @relation(fields: [pageId], references: [id]) + + @@allow('all', true) + } + ` + ); + + const db = enhance(); + + const image = await db.image.create({ + data: { + url: 'https://example.com/image.png', + }, + }); + + await expect( + db.image.update({ + where: { id: image.id }, + data: { + page: { + create: { + title: 'Page 1', + }, + }, + }, + }) + ).toBeRejectedByPolicy(); + }); +}); From 82b8d2533e48578d690dbf2f35f39bb33c44f48a Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Mon, 24 Feb 2025 14:40:33 -0800 Subject: [PATCH 13/13] chore: jetbrains changelog (#2012) --- packages/ide/jetbrains/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md index fb71572bd..936e5e7bd 100644 --- a/packages/ide/jetbrains/CHANGELOG.md +++ b/packages/ide/jetbrains/CHANGELOG.md @@ -4,6 +4,12 @@ ### Added +- Validating regex patterns in ZModel. + +## 2.12.0 + +### Added + - Field encryption attribute `@encrypted`. ## 2.9.3