diff --git a/README.md b/README.md
index d874cfefb..968f5b16a 100644
--- a/README.md
+++ b/README.md
@@ -235,6 +235,8 @@ Thank you for your support!
 Johann Rohn |
 Benjamin Zecirovic |
+  Fabian Jocks |
+
diff --git a/package.json b/package.json
index d61f79174..0bb55596e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "zenstack-monorepo",
- "version": "1.11.0",
+ "version": "1.11.1",
"description": "",
"scripts": {
"build": "pnpm -r build",
diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md
index 1fa15f2eb..226419eb2 100644
--- a/packages/ide/jetbrains/CHANGELOG.md
+++ b/packages/ide/jetbrains/CHANGELOG.md
@@ -1,11 +1,23 @@
# Changelog
## [Unreleased]
+
+### Fixed
+
+- General improvements to language service.
+
+## 1.9.0
+
### Added
-- Added support to complex usage of `@@index` attribute like `@@index([content(ops: raw("gin_trgm_ops"))], type: Gin)`.
+
+- Added support to complex usage of `@@index` attribute like `@@index([content(ops: raw("gin_trgm_ops"))], type: Gin)`.
+
### Fixed
-- Fixed several ZModel validation issues related to model inheritance.
+
+- Fixed several ZModel validation issues related to model inheritance.
## 1.7.0
+
### Added
-- Auto-completion is now supported inside attributes.
+
+- Auto-completion is now supported inside attributes.
diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts
index 23dc11e99..ea192ff0b 100644
--- a/packages/ide/jetbrains/build.gradle.kts
+++ b/packages/ide/jetbrains/build.gradle.kts
@@ -9,7 +9,7 @@ plugins {
}
group = "dev.zenstack"
-version = "1.11.0"
+version = "1.11.1"
repositories {
mavenCentral()
diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json
index 09476b216..564af0351 100644
--- a/packages/ide/jetbrains/package.json
+++ b/packages/ide/jetbrains/package.json
@@ -1,6 +1,6 @@
{
"name": "jetbrains",
- "version": "1.11.0",
+ "version": "1.11.1",
"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 50758ce23..7e8fa9fdc 100644
--- a/packages/language/package.json
+++ b/packages/language/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/language",
- "version": "1.11.0",
+ "version": "1.11.1",
"displayName": "ZenStack modeling language compiler",
"description": "ZenStack modeling language compiler",
"homepage": "https://zenstack.dev",
@@ -9,7 +9,7 @@
"generate": "langium generate && npx ts-node script/generate-plist.ts",
"watch": "concurrently \"langium generate --watch\" \"tsc --watch\"",
"lint": "eslint src --ext ts",
- "build": "pnpm lint --max-warnings=0 && pnpm clean && pnpm generate && tsc && copyfiles -F ./README.md ./LICENSE ./package.json 'syntaxes/**/*' dist && pnpm pack dist --pack-destination '../../../.build'",
+ "build": "pnpm lint --max-warnings=0 && pnpm clean && pnpm generate && tsc && copyfiles -F ./README.md ./LICENSE ./package.json 'syntaxes/**/*' dist && pnpm pack dist --pack-destination ../../../.build",
"prepublishOnly": "pnpm build"
},
"publishConfig": {
diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json
index d95ef1925..33d41a01c 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": "1.11.0",
+ "version": "1.11.1",
"description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.",
"repository": {
"type": "git",
@@ -9,7 +9,7 @@
},
"scripts": {
"clean": "rimraf dist",
- "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && pnpm pack dist --pack-destination '../../../.build'",
+ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && pnpm pack dist --pack-destination ../../../.build",
"watch": "tsc --watch",
"lint": "eslint src --ext ts",
"prepublishOnly": "pnpm build"
diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json
index 3db67dead..38c85462b 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": "1.11.0",
+ "version": "1.11.1",
"description": "ZenStack plugin and runtime supporting OpenAPI",
"main": "index.js",
"repository": {
@@ -14,7 +14,7 @@
},
"scripts": {
"clean": "rimraf dist",
- "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE dist && copyfiles -u 1 ./src/plugin.zmodel dist && pnpm pack dist --pack-destination '../../../../.build'",
+ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE dist && copyfiles -u 1 ./src/plugin.zmodel dist && pnpm pack dist --pack-destination ../../../../.build",
"watch": "tsc --watch",
"lint": "eslint src --ext ts",
"test": "jest",
diff --git a/packages/plugins/openapi/tests/openapi-restful.test.ts b/packages/plugins/openapi/tests/openapi-restful.test.ts
index fb01e390e..9e84ad047 100644
--- a/packages/plugins/openapi/tests/openapi-restful.test.ts
+++ b/packages/plugins/openapi/tests/openapi-restful.test.ts
@@ -4,7 +4,7 @@
import OpenAPIParser from '@readme/openapi-parser';
import { getLiteral, getObjectLiteral } from '@zenstackhq/sdk';
import { Model, Plugin, isPlugin } from '@zenstackhq/sdk/ast';
-import { loadZModelAndDmmf } from '@zenstackhq/testtools';
+import { loadZModelAndDmmf, normalizePath } from '@zenstackhq/testtools';
import fs from 'fs';
import path from 'path';
import * as tmp from 'tmp';
@@ -16,7 +16,7 @@ describe('Open API Plugin RESTful Tests', () => {
for (const specVersion of ['3.0.0', '3.1.0']) {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
specVersion = '${specVersion}'
}
@@ -114,7 +114,7 @@ model Bar {
it('options', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
specVersion = '3.0.0'
title = 'My Awesome API'
version = '1.0.0'
@@ -151,7 +151,7 @@ model User {
it('security schemes valid', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
securitySchemes = {
myBasic: { type: 'http', scheme: 'basic' },
myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
@@ -198,7 +198,7 @@ model Post {
it('security model level override', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
securitySchemes = {
myBasic: { type: 'http', scheme: 'basic' }
}
@@ -230,7 +230,7 @@ model User {
it('security schemes invalid', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
securitySchemes = {
myBasic: { type: 'invalid', scheme: 'basic' }
}
@@ -251,7 +251,7 @@ model User {
it('ignored model used as relation', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
}
model User {
@@ -284,7 +284,7 @@ model Post {
for (const specVersion of ['3.0.0', '3.1.0']) {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
specVersion = '${specVersion}'
}
diff --git a/packages/plugins/openapi/tests/openapi-rpc.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts
index c0cb74ab6..8e8e3a6ac 100644
--- a/packages/plugins/openapi/tests/openapi-rpc.test.ts
+++ b/packages/plugins/openapi/tests/openapi-rpc.test.ts
@@ -4,7 +4,7 @@
import OpenAPIParser from '@readme/openapi-parser';
import { getLiteral, getObjectLiteral } from '@zenstackhq/sdk';
import { Model, Plugin, isPlugin } from '@zenstackhq/sdk/ast';
-import { loadZModelAndDmmf } from '@zenstackhq/testtools';
+import { loadZModelAndDmmf, normalizePath } from '@zenstackhq/testtools';
import fs from 'fs';
import path from 'path';
import * as tmp from 'tmp';
@@ -16,7 +16,7 @@ describe('Open API Plugin RPC Tests', () => {
for (const specVersion of ['3.0.0', '3.1.0']) {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
specVersion = '${specVersion}'
}
@@ -127,7 +127,7 @@ model Bar {
it('options', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
specVersion = '3.0.0'
title = 'My Awesome API'
version = '1.0.0'
@@ -164,7 +164,7 @@ model User {
it('security schemes valid', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
securitySchemes = {
myBasic: { type: 'http', scheme: 'basic' },
myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
@@ -198,7 +198,7 @@ model User {
it('security schemes invalid', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
securitySchemes = {
myBasic: { type: 'invalid', scheme: 'basic' }
}
@@ -219,7 +219,7 @@ model User {
it('security model level override', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
securitySchemes = {
myBasic: { type: 'http', scheme: 'basic' }
}
@@ -247,7 +247,7 @@ model User {
it('security operation level override', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
securitySchemes = {
myBasic: { type: 'http', scheme: 'basic' }
}
@@ -280,7 +280,7 @@ model User {
it('security inferred', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
securitySchemes = {
myBasic: { type: 'http', scheme: 'basic' }
}
@@ -306,7 +306,7 @@ model User {
it('v3.1.0 fields', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
summary = 'awesome api'
}
@@ -330,7 +330,7 @@ model User {
it('ignored model used as relation', async () => {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
}
model User {
@@ -362,7 +362,7 @@ model Post {
for (const specVersion of ['3.0.0', '3.1.0']) {
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
specVersion = '${specVersion}'
}
@@ -408,7 +408,7 @@ generator js {
}
plugin openapi {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
}
enum role {
diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json
index 0d1fb7147..9266a6a36 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": "1.11.0",
+ "version": "1.11.1",
"description": "ZenStack plugin for generating SWR hooks",
"main": "index.js",
"repository": {
@@ -10,7 +10,7 @@
},
"scripts": {
"clean": "rimraf dist",
- "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../../.build'",
+ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination ../../../../.build",
"watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\"",
"lint": "eslint src --ext ts",
"test": "jest",
diff --git a/packages/plugins/swr/tests/swr.test.ts b/packages/plugins/swr/tests/swr.test.ts
index 9d198269b..d12c3b37b 100644
--- a/packages/plugins/swr/tests/swr.test.ts
+++ b/packages/plugins/swr/tests/swr.test.ts
@@ -1,6 +1,6 @@
///
-import { loadSchema } from '@zenstackhq/testtools';
+import { loadSchema, normalizePath } from '@zenstackhq/testtools';
import path from 'path';
describe('SWR Plugin Tests', () => {
@@ -50,7 +50,7 @@ model Foo {
await loadSchema(
`
plugin swr {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/hooks'
}
@@ -60,7 +60,7 @@ ${sharedModel}
provider: 'postgresql',
pushDb: false,
extraDependencies: [
- `${path.join(__dirname, '../dist')}`,
+ `${normalizePath(path.join(__dirname, '../dist'))}`,
'react@18.2.0',
'@types/react@18.2.0',
'swr@^2',
diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json
index 8d034e8f4..51871526a 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": "1.11.0",
+ "version": "1.11.1",
"description": "ZenStack plugin for generating tanstack-query hooks",
"main": "index.js",
"exports": {
@@ -66,7 +66,7 @@
},
"scripts": {
"clean": "rimraf dist",
- "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && tsup-node --config ./tsup-v5.config.ts && node scripts/postbuild && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../../.build'",
+ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && tsup-node --config ./tsup-v5.config.ts && node scripts/postbuild && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination ../../../../.build",
"watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\" \"tsup-node --config ./tsup-v5.config.ts --watch\"",
"lint": "eslint src --ext ts",
"test": "jest",
diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts
index 38370d38a..824174ba5 100644
--- a/packages/plugins/tanstack-query/tests/plugin.test.ts
+++ b/packages/plugins/tanstack-query/tests/plugin.test.ts
@@ -1,6 +1,6 @@
///
-import { loadSchema } from '@zenstackhq/testtools';
+import { loadSchema, normalizePath } from '@zenstackhq/testtools';
import path from 'path';
describe('Tanstack Query Plugin Tests', () => {
@@ -50,7 +50,7 @@ model Foo {
await loadSchema(
`
plugin tanstack {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/hooks'
target = 'react'
}
@@ -71,7 +71,7 @@ ${sharedModel}
await loadSchema(
`
plugin tanstack {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/hooks'
target = 'react'
version = 'v5'
@@ -93,7 +93,7 @@ ${sharedModel}
await loadSchema(
`
plugin tanstack {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/hooks'
target = 'vue'
}
@@ -114,7 +114,7 @@ ${sharedModel}
await loadSchema(
`
plugin tanstack {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/hooks'
target = 'vue'
version = 'v5'
@@ -136,7 +136,7 @@ ${sharedModel}
await loadSchema(
`
plugin tanstack {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/hooks'
target = 'svelte'
}
@@ -157,7 +157,7 @@ ${sharedModel}
await loadSchema(
`
plugin tanstack {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/hooks'
target = 'svelte'
version = 'v5'
diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json
index ad76965c2..e9fddc330 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": "1.11.0",
+ "version": "1.11.1",
"description": "ZenStack plugin for tRPC",
"main": "index.js",
"repository": {
@@ -10,7 +10,7 @@
},
"scripts": {
"clean": "rimraf dist",
- "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE 'res/**/*' dist && pnpm pack dist --pack-destination '../../../../.build'",
+ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE 'res/**/*' dist && pnpm pack dist --pack-destination ../../../../.build",
"watch": "tsc --watch",
"lint": "eslint src --ext ts",
"test": "jest",
diff --git a/packages/plugins/trpc/tests/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts
index ca4a9c14d..4c79c740a 100644
--- a/packages/plugins/trpc/tests/trpc.test.ts
+++ b/packages/plugins/trpc/tests/trpc.test.ts
@@ -1,6 +1,6 @@
///
-import { loadSchema } from '@zenstackhq/testtools';
+import { loadSchema, normalizePath } from '@zenstackhq/testtools';
import fs from 'fs';
import path from 'path';
@@ -19,7 +19,7 @@ describe('tRPC Plugin Tests', () => {
await loadSchema(
`
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/trpc'
}
@@ -67,7 +67,7 @@ model Foo {
const { projectDir } = await loadSchema(
`
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = './trpc'
}
@@ -110,7 +110,7 @@ model Foo {
const { projectDir } = await loadSchema(
`
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = './trpc'
}
@@ -141,7 +141,7 @@ model Post {
const { projectDir } = await loadSchema(
`
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = './trpc'
generateModelActions = 'findMany,findUnique,update'
}
@@ -171,7 +171,7 @@ model Post {
const { projectDir } = await loadSchema(
`
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = './trpc'
generateModelActions = ['findMany', 'findUnique', 'update']
}
@@ -220,7 +220,7 @@ model Post {
await loadSchema(
`
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/trpc'
generateClientHelpers = 'react'
}
@@ -245,7 +245,7 @@ model Post {
await loadSchema(
`
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/trpc'
generateClientHelpers = 'next'
}
@@ -265,7 +265,7 @@ model Post {
await loadSchema(
`
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/trpc'
}
@@ -304,7 +304,7 @@ generator js {
}
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/trpc'
generateModels = ['Post']
generateModelActions = ['findMany', 'update']
@@ -375,7 +375,7 @@ plugin zod {
}
plugin trpc {
- provider = '${path.resolve(__dirname, '../dist')}'
+ provider = '${normalizePath(path.resolve(__dirname, '../dist'))}'
output = '$projectRoot/trpc'
generateModels = ['Post']
generateModelActions = ['findMany', 'update']
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 5665262c1..8d582796e 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/runtime",
"displayName": "ZenStack Runtime Library",
- "version": "1.11.0",
+ "version": "1.11.1",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts
index 808763ae8..322b0261e 100644
--- a/packages/runtime/src/enhancements/policy/handler.ts
+++ b/packages/runtime/src/enhancements/policy/handler.ts
@@ -4,6 +4,7 @@ import { lowerCaseFirst } from 'lower-case-first';
import invariant from 'tiny-invariant';
import { upperCaseFirst } from 'upper-case-first';
import { fromZodError } from 'zod-validation-error';
+import type { WithPolicyOptions } from '.';
import { CrudFailureReason } from '../../constants';
import {
ModelDataVisitor,
@@ -23,7 +24,6 @@ import { formatObject, prismaClientValidationError } from '../utils';
import { Logger } from './logger';
import { PolicyUtil } from './policy-utils';
import { createDeferredPromise } from './promise';
-import { WithPolicyOptions } from '.';
// a record for post-write policy check
type PostWriteCheckRecord = {
@@ -58,6 +58,7 @@ export class PolicyProxyHandler implements Pr
this.logger = new Logger(prisma);
this.utils = new PolicyUtil(
this.prisma,
+ this.options,
this.modelMeta,
this.policy,
this.zodSchemas,
@@ -77,20 +78,20 @@ export class PolicyProxyHandler implements Pr
findUnique(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
if (!args.where) {
- throw prismaClientValidationError(this.prisma, 'where field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument');
}
return this.findWithFluentCallStubs(args, 'findUnique', false, () => null);
}
findUniqueOrThrow(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
if (!args.where) {
- throw prismaClientValidationError(this.prisma, 'where field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument');
}
return this.findWithFluentCallStubs(args, 'findUniqueOrThrow', true, () => {
throw this.utils.notFound(this.model);
@@ -220,10 +221,10 @@ export class PolicyProxyHandler implements Pr
async create(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
if (!args.data) {
- throw prismaClientValidationError(this.prisma, 'data field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument');
}
this.utils.tryReject(this.prisma, this.model, 'create');
@@ -476,10 +477,10 @@ export class PolicyProxyHandler implements Pr
async createMany(args: { data: any; skipDuplicates?: boolean }) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
if (!args.data) {
- throw prismaClientValidationError(this.prisma, 'data field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument');
}
this.utils.tryReject(this.prisma, this.model, 'create');
@@ -596,13 +597,13 @@ export class PolicyProxyHandler implements Pr
async update(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
if (!args.where) {
- throw prismaClientValidationError(this.prisma, 'where field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument');
}
if (!args.data) {
- throw prismaClientValidationError(this.prisma, 'data field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument');
}
args = this.utils.clone(args);
@@ -734,7 +735,10 @@ export class PolicyProxyHandler implements Pr
) => {
for (const item of enumerate(args.data)) {
if (args.skipDuplicates) {
- if (await this.hasDuplicatedUniqueConstraint(model, item, db)) {
+ // get a reversed query to include fields inherited from upstream mutation,
+ // it'll be merged with the create payload for unique constraint checking
+ const reversedQuery = this.utils.buildReversedQuery(context);
+ if (await this.hasDuplicatedUniqueConstraint(model, { ...reversedQuery, ...item }, db)) {
if (this.shouldLogQuery) {
this.logger.info(`[policy] \`createMany\` skipping duplicate ${formatObject(item)}`);
}
@@ -1071,10 +1075,10 @@ export class PolicyProxyHandler implements Pr
async updateMany(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
if (!args.data) {
- throw prismaClientValidationError(this.prisma, 'data field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'data field is required in query argument');
}
this.utils.tryReject(this.prisma, this.model, 'update');
@@ -1130,16 +1134,16 @@ export class PolicyProxyHandler implements Pr
async upsert(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
if (!args.where) {
- throw prismaClientValidationError(this.prisma, 'where field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument');
}
if (!args.create) {
- throw prismaClientValidationError(this.prisma, 'create field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'create field is required in query argument');
}
if (!args.update) {
- throw prismaClientValidationError(this.prisma, 'update field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'update field is required in query argument');
}
this.utils.tryReject(this.prisma, this.model, 'create');
@@ -1183,10 +1187,10 @@ export class PolicyProxyHandler implements Pr
async delete(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
if (!args.where) {
- throw prismaClientValidationError(this.prisma, 'where field is required in query argument');
+ throw prismaClientValidationError(this.prisma, this.options, 'where field is required in query argument');
}
this.utils.tryReject(this.prisma, this.model, 'delete');
@@ -1239,7 +1243,7 @@ export class PolicyProxyHandler implements Pr
async aggregate(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
args = this.utils.clone(args);
@@ -1255,7 +1259,7 @@ export class PolicyProxyHandler implements Pr
async groupBy(args: any) {
if (!args) {
- throw prismaClientValidationError(this.prisma, 'query argument is required');
+ throw prismaClientValidationError(this.prisma, this.options, 'query argument is required');
}
args = this.utils.clone(args);
@@ -1299,7 +1303,7 @@ export class PolicyProxyHandler implements Pr
args = { create: {}, update: {}, delete: {} };
} else {
if (typeof args !== 'object') {
- throw prismaClientValidationError(this.prisma, 'argument must be an object');
+ throw prismaClientValidationError(this.prisma, this.options, 'argument must be an object');
}
if (Object.keys(args).length === 0) {
// include all
diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts
index ea5816f6c..5a04c0430 100644
--- a/packages/runtime/src/enhancements/policy/policy-utils.ts
+++ b/packages/runtime/src/enhancements/policy/policy-utils.ts
@@ -5,6 +5,7 @@ import { lowerCaseFirst } from 'lower-case-first';
import { upperCaseFirst } from 'upper-case-first';
import { ZodError } from 'zod';
import { fromZodError } from 'zod-validation-error';
+import type { EnhancementOptions } from '..';
import {
CrudFailureReason,
FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX,
@@ -48,6 +49,7 @@ export class PolicyUtil {
constructor(
private readonly db: DbClientContract,
+ private readonly options: EnhancementOptions | undefined,
private readonly modelMeta: ModelMeta,
private readonly policy: PolicyDef,
private readonly zodSchemas: ZodSchemas | undefined,
@@ -1098,24 +1100,25 @@ export class PolicyUtil {
return prismaClientKnownRequestError(
this.db,
+ this.options,
`denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`,
args
);
}
notFound(model: string) {
- return prismaClientKnownRequestError(this.db, `entity not found for model ${model}`, {
+ return prismaClientKnownRequestError(this.db, this.options, `entity not found for model ${model}`, {
clientVersion: getVersion(),
code: 'P2025',
});
}
validationError(message: string) {
- return prismaClientValidationError(this.db, message);
+ return prismaClientValidationError(this.db, this.options, message);
}
unknownError(message: string) {
- return prismaClientUnknownRequestError(this.db, message, {
+ return prismaClientUnknownRequestError(this.db, this.options, message, {
clientVersion: getVersion(),
});
}
diff --git a/packages/runtime/src/enhancements/types.ts b/packages/runtime/src/enhancements/types.ts
index 9c8080096..dec8f097e 100644
--- a/packages/runtime/src/enhancements/types.ts
+++ b/packages/runtime/src/enhancements/types.ts
@@ -19,6 +19,13 @@ export interface CommonEnhancementOptions {
* Path for loading CLI-generated code
*/
loadPath?: string;
+
+ /**
+ * The `Prisma` module generated together with `PrismaClient`. You only need to
+ * pass it when you specified a custom `PrismaClient` output path. The module can
+ * be loaded like: `import { Prisma } from '';`.
+ */
+ prismaModule?: any;
}
/**
diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts
index 73b4d42a0..72b1e393d 100644
--- a/packages/runtime/src/enhancements/utils.ts
+++ b/packages/runtime/src/enhancements/utils.ts
@@ -3,6 +3,7 @@
import path from 'path';
import * as util from 'util';
import type { DbClientContract } from '../types';
+import type { EnhancementOptions } from './enhance';
/**
* Formats an object for pretty printing.
@@ -53,25 +54,37 @@ function loadPrismaModule(prisma: any) {
}
}
-export function prismaClientValidationError(prisma: DbClientContract, message: string) {
+export function prismaClientValidationError(
+ prisma: DbClientContract,
+ options: EnhancementOptions | undefined,
+ message: string
+) {
if (!_PrismaClientValidationError) {
- const _prisma = loadPrismaModule(prisma);
+ const _prisma = options?.prismaModule ?? loadPrismaModule(prisma);
_PrismaClientValidationError = _prisma.PrismaClientValidationError;
}
throw new _PrismaClientValidationError(message, { clientVersion: prisma._clientVersion });
}
-export function prismaClientKnownRequestError(prisma: DbClientContract, ...args: unknown[]) {
+export function prismaClientKnownRequestError(
+ prisma: DbClientContract,
+ options: EnhancementOptions | undefined,
+ ...args: unknown[]
+) {
if (!_PrismaClientKnownRequestError) {
- const _prisma = loadPrismaModule(prisma);
+ const _prisma = options?.prismaModule ?? loadPrismaModule(prisma);
_PrismaClientKnownRequestError = _prisma.PrismaClientKnownRequestError;
}
return new _PrismaClientKnownRequestError(...args);
}
-export function prismaClientUnknownRequestError(prisma: DbClientContract, ...args: unknown[]) {
+export function prismaClientUnknownRequestError(
+ prisma: DbClientContract,
+ options: EnhancementOptions | undefined,
+ ...args: unknown[]
+) {
if (!_PrismaClientUnknownRequestError) {
- const _prisma = loadPrismaModule(prisma);
+ const _prisma = options?.prismaModule ?? loadPrismaModule(prisma);
_PrismaClientUnknownRequestError = _prisma.PrismaClientUnknownRequestError;
}
throw new _PrismaClientUnknownRequestError(...args);
diff --git a/packages/schema/package.json b/packages/schema/package.json
index 249389e68..25ebd253b 100644
--- a/packages/schema/package.json
+++ b/packages/schema/package.json
@@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack Language Tools",
"description": "Build scalable web apps with minimum code by defining authorization and validation rules inside the data schema that closer to the database",
- "version": "1.11.0",
+ "version": "1.11.1",
"author": {
"name": "ZenStack Team"
},
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 110fec6c0..5e3707444 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/sdk",
- "version": "1.11.0",
+ "version": "1.11.1",
"description": "ZenStack plugin development SDK",
"main": "index.js",
"scripts": {
diff --git a/packages/server/package.json b/packages/server/package.json
index ea60177a5..699933197 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/server",
- "version": "1.11.0",
+ "version": "1.11.1",
"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 97e842f00..8e68dabc0 100644
--- a/packages/testtools/package.json
+++ b/packages/testtools/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/testtools",
- "version": "1.11.0",
+ "version": "1.11.1",
"description": "ZenStack Test Tools",
"main": "index.js",
"private": true,
diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts
index 88ffa9ba8..5fef95299 100644
--- a/packages/testtools/src/schema.ts
+++ b/packages/testtools/src/schema.ts
@@ -54,7 +54,7 @@ export function installPackage(pkg: string, dev = false) {
run(`npm install ${dev ? '-D' : ''} --no-audit --no-fund ${pkg}`);
}
-function normalizePath(p: string) {
+export function normalizePath(p: string) {
return p ? p.split(path.sep).join(path.posix.sep) : p;
}
@@ -174,6 +174,9 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) {
const files = schema.split(FILE_SPLITTER);
+ // Use this one to replace $projectRoot placeholder in the schema file
+ const normalizedProjectRoot = normalizePath(projectRoot);
+
if (files.length > 1) {
// multiple files
files.forEach((file, index) => {
@@ -190,12 +193,12 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) {
}
}
- fileContent = fileContent.replaceAll('$projectRoot', projectRoot);
+ fileContent = fileContent.replaceAll('$projectRoot', normalizedProjectRoot);
const filePath = path.join(projectRoot, fileName);
fs.writeFileSync(filePath, fileContent);
});
} else {
- schema = schema.replaceAll('$projectRoot', projectRoot);
+ schema = schema.replaceAll('$projectRoot', normalizedProjectRoot);
const content = opt.addPrelude ? `${makePrelude(opt)}\n${schema}` : schema;
if (opt.customSchemaFilePath) {
zmodelPath = path.join(projectRoot, opt.customSchemaFilePath);
diff --git a/tests/integration/tests/enhancements/with-policy/options.test.ts b/tests/integration/tests/enhancements/with-policy/options.test.ts
index 2c661ceb4..9c82e4305 100644
--- a/tests/integration/tests/enhancements/with-policy/options.test.ts
+++ b/tests/integration/tests/enhancements/with-policy/options.test.ts
@@ -1,4 +1,4 @@
-import { withPolicy } from '@zenstackhq/runtime';
+import { enhance } from '@zenstackhq/runtime';
import { loadSchema } from '@zenstackhq/testtools';
import path from 'path';
@@ -20,17 +20,48 @@ describe('Password test', () => {
id String @id @default(cuid())
x Int
+ @@allow('read', true)
@@allow('create', x > 0)
}`,
{ getPrismaOnly: true, output: './zen' }
);
- const db = withPolicy(prisma, undefined, { loadPath: './zen' });
+ const db = enhance(prisma, undefined, { loadPath: './zen' });
await expect(
db.foo.create({
data: { x: 0 },
})
).toBeRejectedByPolicy();
+ await expect(
+ db.foo.create({
+ data: { x: 1 },
+ })
+ ).toResolveTruthy();
+ });
+
+ it('prisma module', async () => {
+ const { prisma, Prisma, modelMeta, policy } = await loadSchema(
+ `
+ model Foo {
+ id String @id @default(cuid())
+ x Int
+
+ @@allow('read', true)
+ @@allow('create', x > 0)
+ }`
+ );
+
+ const db = enhance(prisma, undefined, { modelMeta, policy, prismaModule: Prisma });
+ await expect(
+ db.foo.create({
+ data: { x: 0 },
+ })
+ ).toBeRejectedByPolicy();
+ await expect(
+ db.foo.create({
+ data: { x: 1 },
+ })
+ ).toResolveTruthy();
});
it('overrides', async () => {
@@ -45,7 +76,7 @@ describe('Password test', () => {
{ getPrismaOnly: true, output: './zen' }
);
- const db = withPolicy(prisma, undefined, {
+ const db = enhance(prisma, undefined, {
modelMeta: require(path.resolve('./zen/model-meta')).default,
policy: require(path.resolve('./zen/policy')).default,
});
diff --git a/tests/integration/tests/regression/issue-1162.test.ts b/tests/integration/tests/regression/issue-1162.test.ts
new file mode 100644
index 000000000..fd7f0dded
--- /dev/null
+++ b/tests/integration/tests/regression/issue-1162.test.ts
@@ -0,0 +1,56 @@
+import { loadSchema } from '@zenstackhq/testtools';
+
+describe('issue 1162', () => {
+ it('regression', async () => {
+ const { enhance } = await loadSchema(
+ `
+ model User {
+ id String @id @default(cuid())
+ companies CompanyUser[]
+ @@allow('all', true)
+ }
+
+ model Company {
+ id String @id @default(cuid())
+ users CompanyUser[]
+ @@allow('all', true)
+ }
+
+ model CompanyUser {
+ company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
+ companyId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ userId String
+ @@id([companyId, userId])
+ @@allow('all', true)
+ }
+ `,
+ { logPrismaQuery: true }
+ );
+
+ const db = enhance();
+
+ await db.user.create({ data: { id: 'abc' } });
+ await db.user.create({ data: { id: 'def' } });
+ await db.company.create({ data: { id: '1', users: { create: { userId: 'abc' } } } });
+ await expect(
+ db.company.update({
+ where: { id: '1' },
+ data: {
+ users: {
+ createMany: {
+ data: [{ userId: 'abc' }, { userId: 'def' }],
+ skipDuplicates: true,
+ },
+ },
+ },
+ include: { users: true },
+ })
+ ).resolves.toMatchObject({
+ users: expect.arrayContaining([
+ { companyId: '1', userId: 'abc' },
+ { companyId: '1', userId: 'def' },
+ ]),
+ });
+ });
+});