diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f92512bc8..404f42d3a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -97,4 +97,4 @@ jobs: run: pnpm install --frozen-lockfile - name: Test - run: pnpm run test-ci + run: pnpm run test-scaffold && pnpm run test-ci diff --git a/.gitignore b/.gitignore index 9b5f3d7c8..307f58a86 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist .npmcache coverage .build +.test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1f733983..97319e08c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,14 @@ I want to think you first for considering contributing to ZenStack 🙏🏻. It' pnpm build ``` +1. Scaffold the project used for testing + + ```bash + pnpm test-scaffold + ``` + + You only need to run this command once. + 1. Run tests ```bash diff --git a/jest.config.ts b/jest.config.ts index 917cf52f6..b08a6426f 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,15 +3,21 @@ * https://jestjs.io/docs/configuration */ +import path from 'path'; + export default { // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, + globalSetup: path.join(__dirname, './script/test-global-setup.ts'), + + setupFiles: [path.join(__dirname, './script/set-test-env.ts')], + // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, // The directory where Jest should output its coverage files - coverageDirectory: 'tests/coverage', + coverageDirectory: path.join(__dirname, '.test/coverage'), // An array of regexp pattern strings used to skip coverage collection coveragePathIgnorePatterns: ['/node_modules/', '/tests/'], diff --git a/package.json b/package.json index 2d2d2e264..dbd80b93b 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "zenstack-monorepo", - "version": "1.9.0", + "version": "1.10.0", "description": "", "scripts": { "build": "pnpm -r build", "lint": "pnpm -r lint", - "test": "ZENSTACK_TEST=1 pnpm -r run test --silent --forceExit", - "test-ci": "ZENSTACK_TEST=1 pnpm -r run test --silent --forceExit", + "test": "pnpm -r --parallel run test --silent --forceExit", + "test-ci": "pnpm -r --parallel run test --silent --forceExit", + "test-scaffold": "tsx script/test-scaffold.ts", "publish-all": "pnpm --filter \"./packages/**\" -r publish --access public", "publish-preview": "pnpm --filter \"./packages/**\" -r publish --force --registry https://preview.registry.zenstack.dev/", "unpublish-preview": "pnpm --recursive --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ \"\\$PNPM_PACKAGE_NAME\"" @@ -30,6 +31,7 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "tsup": "^8.0.1", + "tsx": "^4.7.1", "typescript": "^5.3.2" } } diff --git a/packages/README.md b/packages/README.md deleted file mode 100644 index 2104ff0eb..000000000 --- a/packages/README.md +++ /dev/null @@ -1,122 +0,0 @@ -
- -
- - - - - - - - - - - - - -
- -## What it is - -ZenStack is a toolkit that simplifies the development of a web app's backend. It supercharges [Prisma ORM](https://prisma.io) with a powerful access control layer and unleashes its full potential for web development. - -Our goal is to let you save time writing boilerplate code and focus on building real features! - -## How it works - -ZenStack extended Prisma schema language for supporting custom attributes and functions and, based on that, implemented a flexible access control layer around Prisma. - -```prisma -// schema.zmodel - -model Post { - id String @id - title String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String - - // 🔐 allow logged-in users to read published posts - @@allow('read', auth() != null && published) - - // 🔐 allow full CRUD by author - @@allow('all', author == auth()) -} -``` - -At runtime, transparent proxies are created around Prisma clients for intercepting queries and mutations to enforce access policies. Moreover, framework integration packages help you wrap an access-control-enabled Prisma client into backend APIs that can be safely called from the frontend. - -```ts -// Next.js example: pages/api/model/[...path].ts - -import { requestHandler } from '@zenstackhq/next'; -import { withPolicy } from '@zenstackhq/runtime'; -import { getSessionUser } from '@lib/auth'; -import { prisma } from '@lib/db'; - -export default requestHandler({ - getPrisma: (req, res) => withPolicy(prisma, { user: getSessionUser(req, res) }), -}); -``` - -Plugins can generate strong-typed client libraries that talk to the APIs: - -```tsx -// React example: components/MyPosts.tsx - -import { usePost } from '@lib/hooks'; - -const MyPosts = () => { - // Post CRUD hooks - const { findMany } = usePost(); - - // list all posts that're visible to the current user, together with their authors - const { data: posts } = findMany({ - include: { author: true }, - orderBy: { createdAt: 'desc' }, - }); - - return ( - - ); -}; -``` - -## Links - -- [Home](https://zenstack.dev) -- [Documentation](https://zenstack.dev/docs) -- [Community chat](https://go.zenstack.dev/chat) -- [Twitter](https://twitter.com/zenstackhq) -- [Blog](https://dev.to/zenstack) - -## Features - -- Access control and data validation rules right inside your Prisma schema -- Auto-generated RESTful API and client library -- End-to-end type safety -- Extensible: custom attributes, functions, and a plugin system -- Framework agnostic -- Uncompromised performance - -## Examples - -Check out the [Collaborative Todo App](https://zenstack-todo.vercel.app/) for a running example. You can find the source code below: - -- [Next.js + React hooks implementation](https://github.com/zenstackhq/sample-todo-nextjs) -- [Next.js + tRPC implementation](https://github.com/zenstackhq/sample-todo-trpc) - -## Community - -Join our [discord server](https://go.zenstack.dev/chat) for chat and updates! - -## License - -[MIT](LICENSE) diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md index 4f4625001..1fa15f2eb 100644 --- a/packages/ide/jetbrains/CHANGELOG.md +++ b/packages/ide/jetbrains/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog ## [Unreleased] +### Added +- 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. +## 1.7.0 ### Added - Auto-completion is now supported inside attributes. diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts index b3074746e..2e7742364 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.9.0" +version = "1.10.0" repositories { mavenCentral() diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json index 7305853c2..b380dc39f 100644 --- a/packages/ide/jetbrains/package.json +++ b/packages/ide/jetbrains/package.json @@ -1,6 +1,6 @@ { "name": "jetbrains", - "version": "1.9.0", + "version": "1.10.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 a80768913..562ff434e 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.9.0", + "version": "1.10.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 e477bc7b8..2c04bf073 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.9.0", + "version": "1.10.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 96a336f58..c13ecd0c0 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.9.0", + "version": "1.10.0", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { @@ -17,7 +17,7 @@ "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": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "keywords": [ diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index c64538378..733cd6687 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.9.0", + "version": "1.10.0", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { @@ -13,7 +13,7 @@ "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": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 0c16ca59d..05a401f30 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.9.0", + "version": "1.10.0", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { @@ -69,7 +69,7 @@ "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": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 10852e826..3dd040f71 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -216,7 +216,7 @@ function generateMutationHook( { name: `_mutation`, initializer: ` - useModelMutation<${argsType}, ${ + useModelMutation<${argsType}, DefaultError, ${ overrideReturnType ?? model }, ${checkReadBack}>('${model}', '${httpVerb.toUpperCase()}', \`\${endpoint}/${lowerCaseFirst( model @@ -565,9 +565,9 @@ function makeBaseImports(target: TargetFramework, version: TanStackVersion) { const runtimeImportBase = makeRuntimeImportBase(version); const shared = [ `import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '${runtimeImportBase}/${target}';`, - `import type { PickEnumerable, CheckSelect } from '${runtimeImportBase}';`, + `import type { PickEnumerable, CheckSelect, QueryError } from '${runtimeImportBase}';`, `import metadata from './__model_meta';`, - `type DefaultError = Error;`, + `type DefaultError = QueryError;`, ]; switch (target) { case 'react': { @@ -643,11 +643,11 @@ function makeQueryOptions( function makeMutationOptions(target: string, returnType: string, argsType: string) { switch (target) { case 'react': - return `UseMutationOptions<${returnType}, unknown, ${argsType}>`; + return `UseMutationOptions<${returnType}, DefaultError, ${argsType}>`; case 'vue': - return `UseMutationOptions<${returnType}, unknown, ${argsType}, unknown>`; + return `UseMutationOptions<${returnType}, DefaultError, ${argsType}, unknown>`; case 'svelte': - return `MutationOptions<${returnType}, unknown, ${argsType}>`; + return `MutationOptions<${returnType}, DefaultError, ${argsType}>`; default: throw new PluginError(name, `Unsupported target: ${target}`); } diff --git a/packages/plugins/tanstack-query/src/runtime-v5/index.ts b/packages/plugins/tanstack-query/src/runtime-v5/index.ts index 302b775fc..2954d4683 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/index.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/index.ts @@ -1,2 +1,2 @@ export * from '../runtime/prisma-types'; -export { type FetchFn, getQueryKey } from '../runtime/common'; +export { type FetchFn, type QueryError, getQueryKey } from '../runtime/common'; diff --git a/packages/plugins/tanstack-query/src/runtime-v5/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts index 375cb2676..92194535f 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/react.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { + UseSuspenseInfiniteQueryOptions, + UseSuspenseQueryOptions, useInfiniteQuery, useMutation, useQuery, @@ -10,14 +12,11 @@ import { type UseInfiniteQueryOptions, type UseMutationOptions, type UseQueryOptions, - UseSuspenseInfiniteQueryOptions, - UseSuspenseQueryOptions, } from '@tanstack/react-query-v5'; import type { ModelMeta } from '@zenstackhq/runtime/cross'; import { createContext, useContext } from 'react'; import { DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, @@ -25,6 +24,7 @@ import { setupInvalidation, setupOptimisticUpdate, type APIContext, + type FetchFn, } from '../runtime/common'; /** @@ -167,12 +167,18 @@ export function useSuspenseInfiniteModelQuery( * @param checkReadBack Whether to check for read back errors and return undefined if found. * @param optimisticUpdate Whether to enable automatic optimistic update */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, diff --git a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts index 5f479138e..7de2202d6 100644 --- a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts @@ -16,13 +16,13 @@ import { Readable, derived } from 'svelte/store'; import { APIContext, DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, marshal, setupInvalidation, setupOptimisticUpdate, + type FetchFn, } from '../runtime/common'; export { APIContext as RequestHandlerContext } from '../runtime/common'; @@ -147,12 +147,18 @@ function isStore(opt: unknown): opt is Readable { * @param invalidateQueries Whether to invalidate queries after mutation. * @returns useMutation hooks */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, diff --git a/packages/plugins/tanstack-query/src/runtime/common.ts b/packages/plugins/tanstack-query/src/runtime/common.ts index ea64bd3e2..b0d45b246 100644 --- a/packages/plugins/tanstack-query/src/runtime/common.ts +++ b/packages/plugins/tanstack-query/src/runtime/common.ts @@ -25,6 +25,21 @@ export const QUERY_KEY_PREFIX = 'zenstack'; */ export type FetchFn = (url: string, options?: RequestInit) => Promise; +/** + * Type for query and mutation errors. + */ +export type QueryError = Error & { + /** + * Additional error information. + */ + info?: unknown; + + /** + * HTTP status code. + */ + status?: number; +}; + /** * Context type for configuring the hooks. */ @@ -64,9 +79,7 @@ export async function fetcher( // policy doesn't allow mutation result to be read back, just return undefined return undefined as any; } - const error: Error & { info?: unknown; status?: number } = new Error( - 'An error occurred while fetching the data.' - ); + const error: QueryError = new Error('An error occurred while fetching the data.'); error.info = errData.error; error.status = res.status; throw error; diff --git a/packages/plugins/tanstack-query/src/runtime/index.ts b/packages/plugins/tanstack-query/src/runtime/index.ts index 909c0c4bf..0894bc461 100644 --- a/packages/plugins/tanstack-query/src/runtime/index.ts +++ b/packages/plugins/tanstack-query/src/runtime/index.ts @@ -1,2 +1,2 @@ export * from './prisma-types'; -export { type FetchFn, getQueryKey } from './common'; +export { type FetchFn, type QueryError, getQueryKey } from './common'; diff --git a/packages/plugins/tanstack-query/src/runtime/react.ts b/packages/plugins/tanstack-query/src/runtime/react.ts index 2f75d88eb..607b57430 100644 --- a/packages/plugins/tanstack-query/src/runtime/react.ts +++ b/packages/plugins/tanstack-query/src/runtime/react.ts @@ -12,7 +12,6 @@ import type { ModelMeta } from '@zenstackhq/runtime/cross'; import { createContext, useContext } from 'react'; import { DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, @@ -20,6 +19,7 @@ import { setupInvalidation, setupOptimisticUpdate, type APIContext, + type FetchFn, } from './common'; /** @@ -110,12 +110,18 @@ export function useInfiniteModelQuery( * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useMutation hooks */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, diff --git a/packages/plugins/tanstack-query/src/runtime/svelte.ts b/packages/plugins/tanstack-query/src/runtime/svelte.ts index 88c675a82..dbd0342aa 100644 --- a/packages/plugins/tanstack-query/src/runtime/svelte.ts +++ b/packages/plugins/tanstack-query/src/runtime/svelte.ts @@ -13,13 +13,13 @@ import { getContext, setContext } from 'svelte'; import { APIContext, DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, marshal, setupInvalidation, setupOptimisticUpdate, + type FetchFn, } from './common'; export { APIContext as RequestHandlerContext } from './common'; @@ -109,12 +109,18 @@ export function useInfiniteModelQuery( * @param optimisticUpdate Whether to enable automatic optimistic update. * @returns useMutation hooks */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, diff --git a/packages/plugins/tanstack-query/src/runtime/vue.ts b/packages/plugins/tanstack-query/src/runtime/vue.ts index a0f1055e8..049b66907 100644 --- a/packages/plugins/tanstack-query/src/runtime/vue.ts +++ b/packages/plugins/tanstack-query/src/runtime/vue.ts @@ -14,13 +14,13 @@ import { inject, provide } from 'vue'; import { APIContext, DEFAULT_QUERY_ENDPOINT, - FetchFn, fetcher, getQueryKey, makeUrl, marshal, setupInvalidation, setupOptimisticUpdate, + type FetchFn, } from './common'; export { APIContext as RequestHandlerContext } from './common'; @@ -61,7 +61,7 @@ export function useModelQuery( model: string, url: string, args?: unknown, - options?: UseQueryOptions, + options?: Omit, 'queryKey'>, fetch?: FetchFn, optimisticUpdate = false ) { @@ -87,7 +87,7 @@ export function useInfiniteModelQuery( model: string, url: string, args?: unknown, - options?: UseInfiniteQueryOptions, + options?: Omit, 'queryKey'>, fetch?: FetchFn ) { return useInfiniteQuery({ @@ -113,12 +113,18 @@ export function useInfiniteModelQuery( * @param optimisticUpdate Whether to enable automatic optimistic update * @returns useMutation hooks */ -export function useModelMutation( +export function useModelMutation< + TArgs, + TError, + R = any, + C extends boolean = boolean, + Result = C extends true ? R | undefined : R +>( model: string, method: 'POST' | 'PUT' | 'DELETE', url: string, modelMeta: ModelMeta, - options?: Omit, 'mutationFn'>, + options?: Omit, 'mutationFn'>, fetch?: FetchFn, invalidateQueries = true, checkReadBack?: C, @@ -168,5 +174,5 @@ export function useModelMutation(finalOptions); + return useMutation(finalOptions); } diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 0b53d2aa2..44422a105 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.9.0", + "version": "1.10.0", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { @@ -13,7 +13,7 @@ "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": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 3cb61dd38..d4fb0b2a8 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.9.0", + "version": "1.10.0", "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 698dcd364..e11379cdf 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -467,7 +467,7 @@ export class PolicyProxyHandler implements Pr // Validates the given create payload against Zod schema if any private validateCreateInputSchema(model: string, data: any) { const schema = this.utils.getZodSchema(model, 'create'); - if (schema) { + if (schema && data) { const parseResult = schema.safeParse(data); if (!parseResult.success) { throw this.utils.deniedByPolicy( @@ -496,11 +496,18 @@ export class PolicyProxyHandler implements Pr args = this.utils.clone(args); - // do static input validation and check if post-create checks are needed + // go through create items, statically check input to determine if post-create + // check is needed, and also validate zod schema let needPostCreateCheck = false; for (const item of enumerate(args.data)) { + const validationResult = this.validateCreateInputSchema(this.model, item); + if (validationResult !== item) { + this.utils.replace(item, validationResult); + } + const inputCheck = this.utils.checkInputGuard(this.model, item, 'create'); if (inputCheck === false) { + // unconditionally deny throw this.utils.deniedByPolicy( this.model, 'create', @@ -508,14 +515,10 @@ export class PolicyProxyHandler implements Pr CrudFailureReason.ACCESS_POLICY_VIOLATION ); } else if (inputCheck === true) { - const r = this.validateCreateInputSchema(this.model, item); - if (r !== item) { - this.utils.replace(item, r); - } + // unconditionally allow } else if (inputCheck === undefined) { // static policy check is not possible, need to do post-create check needPostCreateCheck = true; - break; } } @@ -786,7 +789,13 @@ export class PolicyProxyHandler implements Pr // check if the update actually writes to this model let thisModelUpdate = false; - const updatePayload: any = (args as any).data ?? args; + const updatePayload = (args as any).data ?? args; + + const validatedPayload = this.validateUpdateInputSchema(model, updatePayload); + if (validatedPayload !== updatePayload) { + this.utils.replace(updatePayload, validatedPayload); + } + if (updatePayload) { for (const key of Object.keys(updatePayload)) { const field = resolveField(this.modelMeta, model, key); @@ -857,6 +866,8 @@ export class PolicyProxyHandler implements Pr ); } + args.data = this.validateUpdateInputSchema(model, args.data); + const updateGuard = this.utils.getAuthGuard(db, model, 'update'); if (this.utils.isTrue(updateGuard) || this.utils.isFalse(updateGuard)) { // injects simple auth guard into where clause @@ -917,7 +928,10 @@ export class PolicyProxyHandler implements Pr await _registerPostUpdateCheck(model, uniqueFilter); // convert upsert to update - context.parent.update = { where: args.where, data: args.update }; + context.parent.update = { + where: args.where, + data: this.validateUpdateInputSchema(model, args.update), + }; delete context.parent.upsert; // continue visiting the new payload @@ -1016,6 +1030,37 @@ export class PolicyProxyHandler implements Pr return { result, postWriteChecks }; } + // Validates the given update payload against Zod schema if any + private validateUpdateInputSchema(model: string, data: any) { + const schema = this.utils.getZodSchema(model, 'update'); + if (schema && data) { + // update payload can contain non-literal fields, like: + // { x: { increment: 1 } } + // we should only validate literal fields + + const literalData = Object.entries(data).reduce( + (acc, [k, v]) => ({ ...acc, ...(typeof v !== 'object' ? { [k]: v } : {}) }), + {} + ); + + const parseResult = schema.safeParse(literalData); + if (!parseResult.success) { + throw this.utils.deniedByPolicy( + model, + 'update', + `input failed validation: ${fromZodError(parseResult.error)}`, + CrudFailureReason.DATA_VALIDATION_VIOLATION, + parseResult.error + ); + } + + // schema may have transformed field values, use it to overwrite the original data + return { ...data, ...parseResult.data }; + } else { + return data; + } + } + private isUnsafeMutate(model: string, args: any) { if (!args) { return false; @@ -1046,6 +1091,8 @@ export class PolicyProxyHandler implements Pr args = this.utils.clone(args); this.utils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); + args.data = this.validateUpdateInputSchema(this.model, args.data); + if (this.utils.hasAuthGuard(this.model, 'postUpdate') || this.utils.getZodSchema(this.model)) { // use a transaction to do post-update checks const postWriteChecks: PostWriteCheckRecord[] = []; diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 63b83b79f..ea5816f6c 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -319,7 +319,7 @@ export class PolicyUtil { /** * Checks if the given model has a policy guard for the given operation. */ - hasAuthGuard(model: string, operation: PolicyOperationKind): boolean { + hasAuthGuard(model: string, operation: PolicyOperationKind) { const guard = this.policy.guard[lowerCaseFirst(model)]; if (!guard) { return false; @@ -328,6 +328,21 @@ export class PolicyUtil { return typeof provider !== 'boolean' || provider !== true; } + /** + * Checks if the given model has any field-level override policy guard for the given operation. + */ + hasOverrideAuthGuard(model: string, operation: PolicyOperationKind) { + const guard = this.requireGuard(model); + switch (operation) { + case 'read': + return Object.keys(guard).some((k) => k.startsWith(FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX)); + case 'update': + return Object.keys(guard).some((k) => k.startsWith(FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX)); + default: + return false; + } + } + /** * Checks model creation policy based on static analysis to the input args. * @@ -731,7 +746,7 @@ export class PolicyUtil { preValue?: any ) { let guard = this.getAuthGuard(db, model, operation, preValue); - if (this.isFalse(guard)) { + if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { throw this.deniedByPolicy( model, operation, @@ -904,7 +919,7 @@ export class PolicyUtil { */ tryReject(db: Record, model: string, operation: PolicyOperationKind) { const guard = this.getAuthGuard(db, model, operation); - if (this.isFalse(guard)) { + if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { throw this.deniedByPolicy(model, operation, undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); } } diff --git a/packages/schema/package.json b/packages/schema/package.json index 2451a87c8..0edd09d95 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.9.0", + "version": "1.10.0", "author": { "name": "ZenStack Team" }, @@ -73,7 +73,7 @@ "bundle": "rimraf bundle && pnpm lint --max-warnings=0 && node build/bundle.js --minify", "watch": "tsc --watch", "lint": "eslint src tests --ext ts", - "test": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build", "postinstall": "node bin/post-install.js" }, diff --git a/packages/schema/src/cli/actions/repl.ts b/packages/schema/src/cli/actions/repl.ts index 6ca3c3503..df15e30fb 100644 --- a/packages/schema/src/cli/actions/repl.ts +++ b/packages/schema/src/cli/actions/repl.ts @@ -2,7 +2,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import colors from 'colors'; import path from 'path'; -import prettyRepl from 'pretty-repl'; import { inspect } from 'util'; // inspired by: https://github.com/Kinjalrk2k/prisma-console @@ -11,6 +10,13 @@ import { inspect } from 'util'; * CLI action for starting a REPL session */ export async function repl(projectPath: string, options: { prismaClient?: string; debug?: boolean; table?: boolean }) { + if (!process?.stdout?.isTTY && process?.versions?.bun) { + console.error('REPL on Bun is only available in a TTY terminal at this time. Please use npm/npx to run the command in this context instead of bun/bunx.'); + return; + } + + const prettyRepl = await import('pretty-repl') + console.log('Welcome to ZenStack REPL. See help with the ".help" command.'); console.log('Global variables:'); console.log(` ${colors.blue('db')} to access enhanced PrismaClient`); diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 85c38e82a..c8ede65a2 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -13,7 +13,7 @@ import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../language-server/cons import { ZModelFormatter } from '../language-server/zmodel-formatter'; import { createZModelServices, ZModelServices } from '../language-server/zmodel-module'; import { mergeBaseModel, resolveImport, resolveTransitiveImports } from '../utils/ast-utils'; -import { findPackageJson } from '../utils/pkg-utils'; +import { findUp } from '../utils/pkg-utils'; import { getVersion } from '../utils/version-utils'; import { CliError } from './cli-error'; @@ -280,7 +280,7 @@ export async function formatDocument(fileName: string) { export function getDefaultSchemaLocation() { // handle override from package.json - const pkgJsonPath = findPackageJson(); + const pkgJsonPath = findUp(['package.json']); if (pkgJsonPath) { const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); if (typeof pkgJson?.zenstack?.schema === 'string') { diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 3096d5257..09af0971c 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -5,6 +5,7 @@ import { isDataModel, isStringLiteral, ReferenceExpr, + isEnum, } from '@zenstackhq/language/ast'; import { getLiteral, getModelIdFields, getModelUniqueFields } from '@zenstackhq/sdk'; import { AstNode, DiagnosticInfo, getDocument, ValidationAcceptor } from 'langium'; @@ -61,8 +62,13 @@ export default class DataModelValidator implements AstValidator { if (idField.type.optional) { accept('error', 'Field with @id attribute must not be optional', { node: idField }); } - if (idField.type.array || !idField.type.type || !SCALAR_TYPES.includes(idField.type.type)) { - accept('error', 'Field with @id attribute must be of scalar type', { node: idField }); + + const isArray = idField.type.array; + const isScalar = SCALAR_TYPES.includes(idField.type.type as (typeof SCALAR_TYPES)[number]); + const isValidType = isScalar || isEnum(idField.type.reference?.ref); + + if (isArray || !isValidType) { + accept('error', 'Field with @id attribute must be of scalar or enum type', { node: idField }); } }); } @@ -115,7 +121,7 @@ export default class DataModelValidator implements AstValidator { fields = (arg.value as ArrayExpr).items as ReferenceExpr[]; if (fields.length === 0) { if (accept) { - accept('error', `"fields" value cannot be emtpy`, { + accept('error', `"fields" value cannot be empty`, { node: arg, }); } @@ -125,7 +131,7 @@ export default class DataModelValidator implements AstValidator { references = (arg.value as ArrayExpr).items as ReferenceExpr[]; if (references.length === 0) { if (accept) { - accept('error', `"references" value cannot be emtpy`, { + accept('error', `"references" value cannot be empty`, { node: arg, }); } @@ -151,6 +157,17 @@ export default class DataModelValidator implements AstValidator { } } else { for (let i = 0; i < fields.length; i++) { + if (!field.type.optional && fields[i].$resolvedType?.nullable) { + // if relation is not optional, then fk field must not be nullable + if (accept) { + accept( + 'error', + `relation "${field.name}" is not optional, but field "${fields[i].target.$refText}" is optional`, + { node: fields[i].target.ref! } + ); + } + } + if (!fields[i].$resolvedType) { if (accept) { accept('error', `field reference is unresolved`, { node: fields[i] }); diff --git a/packages/schema/src/language-server/validator/schema-validator.ts b/packages/schema/src/language-server/validator/schema-validator.ts index b80bf890d..6f868c614 100644 --- a/packages/schema/src/language-server/validator/schema-validator.ts +++ b/packages/schema/src/language-server/validator/schema-validator.ts @@ -52,8 +52,9 @@ export default class SchemaValidator implements AstValidator { private validateImports(model: Model, accept: ValidationAcceptor) { model.imports.forEach((imp) => { const importedModel = resolveImport(this.documents, imp); + const importPath = imp.path.endsWith('.zmodel') ? imp.path : `${imp.path}.zmodel`; if (!importedModel) { - accept('error', `Cannot find model file ${imp.path}.zmodel`, { node: imp }); + accept('error', `Cannot find model file ${importPath}`, { node: imp }); } }); } diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 2aa426b57..0eeea55c5 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -47,8 +47,8 @@ import stripColor from 'strip-color'; import { name } from '.'; import { getStringLiteral } from '../../language-server/validator/utils'; import telemetry from '../../telemetry'; -import { execSync } from '../../utils/exec-utils'; -import { findPackageJson } from '../../utils/pkg-utils'; +import { execPackage } from '../../utils/exec-utils'; +import { findUp } from '../../utils/pkg-utils'; import { ModelFieldType, AttributeArg as PrismaAttributeArg, @@ -127,7 +127,7 @@ export default class PrismaSchemaGenerator { if (options.format === true) { try { // run 'prisma format' - await execSync(`npx prisma format --schema ${outFile}`); + await execPackage(`prisma format --schema ${outFile}`); } catch { warnings.push(`Failed to format Prisma schema file`); } @@ -136,18 +136,18 @@ export default class PrismaSchemaGenerator { const generateClient = options.generateClient !== false; if (generateClient) { - let generateCmd = `npx prisma generate --schema "${outFile}"`; + let generateCmd = `prisma generate --schema "${outFile}"`; if (typeof options.generateArgs === 'string') { generateCmd += ` ${options.generateArgs}`; } try { // run 'prisma generate' - await execSync(generateCmd, 'ignore'); + await execPackage(generateCmd, 'ignore'); } catch { await this.trackPrismaSchemaError(outFile); try { // run 'prisma generate' again with output to the console - await execSync(generateCmd); + await execPackage(generateCmd); } catch { // noop } @@ -450,7 +450,7 @@ export default class PrismaSchemaGenerator { export function getDefaultPrismaOutputFile(schemaPath: string) { // handle override from package.json - const pkgJsonPath = findPackageJson(path.dirname(schemaPath)); + const pkgJsonPath = findUp(['package.json'], path.dirname(schemaPath)); if (pkgJsonPath) { const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); if (typeof pkgJson?.zenstack?.prisma === 'string') { diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts index 39e7d2bb2..889ab1674 100644 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ b/packages/schema/src/plugins/zod/utils/schema-gen.ts @@ -174,7 +174,7 @@ function makeZodSchema(field: DataModelField) { schema = 'z.boolean()'; break; case 'DateTime': - schema = 'z.date()'; + schema = 'z.coerce.date()'; break; case 'Bytes': schema = 'z.union([z.string(), z.instanceof(Uint8Array)])'; diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index a3bee6b2e..145ffed60 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -390,7 +390,7 @@ attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma /** * Add full text index (MySQL only). */ -attribute @@fulltext(_ fields: FieldReference[]) @@@prisma +attribute @@fulltext(_ fields: FieldReference[], map: String?) @@@prisma // String type modifiers @@ -479,7 +479,7 @@ attribute @db.Bytes() @@@targetField([BytesField]) @@@prisma attribute @db.ByteA() @@@targetField([BytesField]) @@@prisma attribute @db.LongBlob() @@@targetField([BytesField]) @@@prisma attribute @db.Binary() @@@targetField([BytesField]) @@@prisma -attribute @db.VarBinary() @@@targetField([BytesField]) @@@prisma +attribute @db.VarBinary(_ x: Int?) @@@targetField([BytesField]) @@@prisma attribute @db.TinyBlob() @@@targetField([BytesField]) @@@prisma attribute @db.Blob() @@@targetField([BytesField]) @@@prisma attribute @db.MediumBlob() @@@targetField([BytesField]) @@@prisma diff --git a/packages/schema/src/utils/exec-utils.ts b/packages/schema/src/utils/exec-utils.ts index f355ae2b4..8f0508dbb 100644 --- a/packages/schema/src/utils/exec-utils.ts +++ b/packages/schema/src/utils/exec-utils.ts @@ -7,3 +7,12 @@ export function execSync(cmd: string, stdio: StdioOptions = 'inherit', env?: Rec const mergedEnv = { ...process.env, ...env }; _exec(cmd, { encoding: 'utf-8', stdio, env: mergedEnv }); } + +/** + * Utility for running package commands through npx/bunx + */ +export function execPackage(cmd: string, stdio: StdioOptions = 'inherit', env?: Record): void { + const packageManager = process?.versions?.bun ? 'bunx' : 'npx'; + const mergedEnv = { ...process.env, ...env }; + _exec(`${packageManager} ${cmd}`, { encoding: 'utf-8', stdio, env: mergedEnv }); +} \ No newline at end of file diff --git a/packages/schema/src/utils/pkg-utils.ts b/packages/schema/src/utils/pkg-utils.ts index ca4ca127d..ce41dac34 100644 --- a/packages/schema/src/utils/pkg-utils.ts +++ b/packages/schema/src/utils/pkg-utils.ts @@ -1,20 +1,40 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import { execSync } from './exec-utils'; export type PackageManagers = 'npm' | 'yarn' | 'pnpm'; -function findUp(names: string[], cwd: string): string | undefined { - let dir = cwd; - // eslint-disable-next-line no-constant-condition - while (true) { - const target = names.find((name) => fs.existsSync(path.join(dir, name))); - if (target) return target; - - const up = path.resolve(dir, '..'); - if (up === dir) return undefined; // it'll fail anyway - dir = up; - } +/** + * A type named FindUp that takes a type parameter e which extends boolean. + * If e extends true, it returns a union type of string[] or undefined. + * If e does not extend true, it returns a union type of string or undefined. + * + * @export + * @template e A type parameter that extends boolean + */ +export type FindUp = e extends true ? string[] | undefined : string | undefined +/** + * Find and return file paths by searching parent directories based on the given names list and current working directory (cwd) path. + * Optionally return a single path or multiple paths. + * If multiple allowed, return all paths found. + * If no paths are found, return undefined. + * + * @export + * @template [e=false] + * @param names An array of strings representing names to search for within the directory + * @param cwd A string representing the current working directory + * @param [multiple=false as e] A boolean flag indicating whether to search for multiple levels. Useful for finding node_modules directories... + * @param [result=[]] An array of strings representing the accumulated results used in multiple results + * @returns Path(s) to a specific file or folder within the directory or parent directories + */ +export function findUp(names: string[], cwd: string = process.cwd(), multiple: e = false as e, result: string[] = []): FindUp { + if (!names.some((name) => !!name)) return undefined; + const target = names.find((name) => fs.existsSync(path.join(cwd, name))); + if (multiple == false && target) return path.join(cwd, target) as FindUp; + if (target) result.push(path.join(cwd, target)); + const up = path.resolve(cwd, '..'); + if (up === cwd) return (multiple && result.length > 0 ? result : undefined) as FindUp; // it'll fail anyway + return findUp(names, up, multiple, result); } function getPackageManager(projectPath = '.'): PackageManagers { @@ -85,6 +105,11 @@ export function ensurePackage( } } +/** + * A function that searches for the nearest package.json file starting from the provided search path or the current working directory if no search path is provided. + * It iterates through the directory structure going one level up at a time until it finds a package.json file. If no package.json file is found, it returns undefined. + * @deprecated Use findUp instead @see findUp + */ export function findPackageJson(searchPath?: string) { let currDir = searchPath ?? process.cwd(); while (currDir) { @@ -102,7 +127,7 @@ export function findPackageJson(searchPath?: string) { } export function getPackageJson(searchPath?: string) { - const pkgJsonPath = findPackageJson(searchPath); + const pkgJsonPath = findUp(['package.json'], searchPath ?? process.cwd()); if (pkgJsonPath) { return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); } else { diff --git a/packages/schema/tests/schema/stdlib.test.ts b/packages/schema/tests/schema/stdlib.test.ts index f4b1cc1fe..ad637be7a 100644 --- a/packages/schema/tests/schema/stdlib.test.ts +++ b/packages/schema/tests/schema/stdlib.test.ts @@ -24,7 +24,7 @@ describe('Stdlib Tests', () => { }` ); } - throw new SchemaLoadingError(validationErrors.map((e) => e.message)); + throw new SchemaLoadingError(validationErrors); } }); }); diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index b86637b58..8eb674b2f 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -226,6 +226,25 @@ describe('Attribute tests', () => { } `); + await loadModel(` + ${ prelude } + model A { + id String @id + x String + y String + z String + @@fulltext([x, y, z]) + } + + model B { + id String @id + x String + y String + z String + @@fulltext([x, y, z], map: "n") + } + `); + await loadModel(` ${prelude} model A { @@ -352,6 +371,7 @@ describe('Attribute tests', () => { _longBlob Bytes @db.LongBlob _binary Bytes @db.Binary _varBinary Bytes @db.VarBinary + _varBinarySized Bytes @db.VarBinary(100) _tinyBlob Bytes @db.TinyBlob _blob Bytes @db.Blob _mediumBlob Bytes @db.MediumBlob diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index 4212441fe..78e31204d 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -1,4 +1,4 @@ -import { loadModel, loadModelWithError } from '../../utils'; +import { loadModel, safelyLoadModel, errorLike } from '../../utils'; describe('Data Model Validation Tests', () => { const prelude = ` @@ -9,20 +9,20 @@ describe('Data Model Validation Tests', () => { `; it('duplicated fields', async () => { - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int - x String - } + const result = await safelyLoadModel(` + ${ prelude } + model M { + id String @id + x Int + x String + } `) - ).toContain('Duplicated declaration name "x"'); + + expect(result).toMatchObject(errorLike('Duplicated declaration name "x"')); }); it('scalar types', async () => { - await loadModel(` + const result = await safelyLoadModel(` ${prelude} model M { id String @id @@ -38,33 +38,36 @@ describe('Data Model Validation Tests', () => { i Bytes } `); + expect(result).toMatchObject({ status: 'fulfilled' }); }); it('Unsupported type valid arg', async () => { - await loadModel(` + const result = await safelyLoadModel(` ${prelude} model M { id String @id a Unsupported('foo') } `); + + expect(result).toMatchObject({ status: 'fulfilled' }); }); it('Unsupported type invalid arg', async () => { expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id a Unsupported(123) } `) - ).toContain('Unsupported type argument must be a string literal'); + ).toMatchObject(errorLike('Unsupported type argument must be a string literal')); }); it('Unsupported type used in expression', async () => { expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id @@ -72,208 +75,258 @@ describe('Data Model Validation Tests', () => { @@allow('all', a == 'a') } `) - ).toContain('Field of "Unsupported" type cannot be used in expressions'); + ).toMatchObject(errorLike('Field of "Unsupported" type cannot be used in expressions')); }); it('mix array and optional', async () => { expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id x Int[]? } `) - ).toContain('Optional lists are not supported. Use either `Type[]` or `Type?`'); + ).toMatchObject(errorLike('Optional lists are not supported. Use either `Type[]` or `Type?`')); }); it('unresolved field type', async () => { expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id x Integer } `) - ).toContain(`Could not resolve reference to TypeDeclaration named 'Integer'.`); + ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id x Integer[] } `) - ).toContain(`Could not resolve reference to TypeDeclaration named 'Integer'.`); + ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model M { id String @id x Integer? } `) - ).toContain(`Could not resolve reference to TypeDeclaration named 'Integer'.`); + ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); }); - it('id field', async () => { + describe('id field', () => { const err = 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.'; - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int - @@allow('all', x > 0) - } - `) - ).toContain(err); + it('should error when there are no unique fields', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int + @@allow('all', x > 0) + } + `) + expect(result).toMatchObject(errorLike(err)); + }) - // @unique used as id - await loadModel(` - ${prelude} - model M { - id Int @unique - x Int - @@allow('all', x > 0) - } - `); + it('should should use @unique when there is no @id', async () => { + const result = await safelyLoadModel(` + ${prelude} + model M { + id Int @unique + x Int + @@allow('all', x > 0) + } + `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }) // @@unique used as id - await loadModel(` - ${prelude} + it('should suceed when @@unique used as id', async () => { + const result = await safelyLoadModel(` + ${ prelude } model M { x Int @@unique([x]) @@allow('all', x > 0) } `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }) + + it('should succeed when @id is an enum type', async () => { + const result = await safelyLoadModel(` + ${ prelude } + enum E { + A + B + } + model M { + id E @id + } + `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }) + + it('should succeed when @@id is an enum type', async () => { + const result = await safelyLoadModel(` + ${ prelude } + enum E { + A + B + } + model M { + x Int + y E + @@id([x, y]) + } + `); + expect(result).toMatchObject({ status: 'fulfilled' }); + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int - @@deny('all', x <= 0) - } - `) - ).toContain(err); + it('should error when there are no id fields, even when denying access', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int + @@deny('all', x <= 0) + } + `) + + expect(result).toMatchObject(errorLike(err)); + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int @gt(0) - } - `) - ).toContain(err); + it('should error when there are not id fields, without access restrictions', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int @gt(0) + } + `) + + expect(result).toMatchObject(errorLike(err)); + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int @id - y Int @id - } - `) - ).toContain(`Model can include at most one field with @id attribute`); + it('should error when there is more than one field marked as @id', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int @id + y Int @id + } + `) + expect(result).toMatchObject(errorLike(`Model can include at most one field with @id attribute`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int @id - y Int - @@id([x, y]) - } - `) - ).toContain(`Model cannot have both field-level @id and model-level @@id attributes`); + it('should error when both @id and @@id are used', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int @id + y Int + @@id([x, y]) + } + `) + expect(result).toMatchObject(errorLike(`Model cannot have both field-level @id and model-level @@id attributes`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int? @id - } - `) - ).toContain(`Field with @id attribute must not be optional`); + it('should error when @id used on optional field', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int? @id + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int? - @@id([x]) - } - `) - ).toContain(`Field with @id attribute must not be optional`); + it('should error when @@id used on optional field', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int? + @@id([x]) + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int[] @id - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @id used on list field', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int[] @id + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Int[] - @@id([x]) - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @@id used on list field', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Int[] + @@id([x]) + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Json @id - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @id used on a Json field', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Json @id + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model M { - x Json - @@id([x]) - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @@id used on a Json field', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model M { + x Json + @@id([x]) + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model Id { - id String @id - } - model M { - myId Id @id - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @id used on a reference field', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model Id { + id String @id + } + model M { + myId Id @id + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) + }) - expect( - await loadModelWithError(` - ${prelude} - model Id { - id String @id - } - model M { - myId Id - @@id([myId]) - } - `) - ).toContain(`Field with @id attribute must be of scalar type`); + it('should error when @@id used on a reference field', async () => { + const result = await safelyLoadModel(` + ${ prelude } + model Id { + id String @id + } + model M { + myId Id + @@id([myId]) + } + `) + expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)) + }) }); it('relation', async () => { @@ -326,7 +379,7 @@ describe('Data Model Validation Tests', () => { // one-to-one incomplete expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -337,11 +390,11 @@ describe('Data Model Validation Tests', () => { id String @id } `) - ).toContain(`The relation field "b" on model "A" is missing an opposite relation field on model "B"`); + ).toMatchObject(errorLike(`The relation field "b" on model "A" is missing an opposite relation field on model "B"`)); // one-to-one ambiguous expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -354,11 +407,11 @@ describe('Data Model Validation Tests', () => { a1 A } `) - ).toContain(`Fields "a", "a1" on model "B" refer to the same relation to model "A"`); + ).toMatchObject(errorLike(`Fields "a", "a1" on model "B" refer to the same relation to model "A"`)); // fields or references missing expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -371,11 +424,11 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toContain(`Both "fields" and "references" must be provided`); + ).toMatchObject(errorLike(`Both "fields" and "references" must be provided`)); // one-to-one inconsistent attribute expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -388,11 +441,11 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toContain(`"fields" and "references" must be provided only on one side of relation field`); + ).toMatchObject(errorLike(`"fields" and "references" must be provided only on one side of relation field`)); // references mismatch expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { myId Int @id @@ -405,11 +458,11 @@ describe('Data Model Validation Tests', () => { aId String @unique } `) - ).toContain(`values of "references" and "fields" must have the same type`); + ).toMatchObject(errorLike(`values of "references" and "fields" must have the same type`)); // "fields" and "references" typing consistency expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id Int @id @@ -422,11 +475,11 @@ describe('Data Model Validation Tests', () => { aId String @unique } `) - ).toContain(`values of "references" and "fields" must have the same type`); + ).toMatchObject(errorLike(`values of "references" and "fields" must have the same type`)); // one-to-one missing @unique expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -439,13 +492,11 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toContain( - `Field "aId" is part of a one-to-one relation and must be marked as @unique or be part of a model-level @@unique attribute` - ); + ).toMatchObject(errorLike(`Field "aId" is part of a one-to-one relation and must be marked as @unique or be part of a model-level @@unique attribute`)); // missing @relation expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -457,13 +508,11 @@ describe('Data Model Validation Tests', () => { a A } `) - ).toContain( - `Field for one side of relation must carry @relation attribute with both "fields" and "references" fields` - ); + ).toMatchObject(errorLike(`Field for one side of relation must carry @relation attribute with both "fields" and "references" fields`)); // wrong relation owner field type expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -476,11 +525,11 @@ describe('Data Model Validation Tests', () => { aId String } `) - ).toContain(`Relation field needs to be list or optional`); + ).toMatchObject(errorLike(`Relation field needs to be list or optional`)); // unresolved field expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model A { id String @id @@ -492,7 +541,7 @@ describe('Data Model Validation Tests', () => { a A @relation(fields: [aId], references: [id]) } `) - ).toContain(`Could not resolve reference to ReferenceTarget named 'aId'.`); + ).toMatchObject(errorLike(`Could not resolve reference to ReferenceTarget named 'aId'.`)); // enum as foreign key await loadModel(` @@ -607,7 +656,7 @@ describe('Data Model Validation Tests', () => { }); it('abstract base type', async () => { - const errors = await loadModelWithError(` + const errors = await safelyLoadModel(` ${prelude} abstract model Base { @@ -623,11 +672,11 @@ describe('Data Model Validation Tests', () => { } `); - expect(errors).toContain(`Model A cannot be extended because it's not abstract`); + expect(errors).toMatchObject(errorLike(`Model A cannot be extended because it's not abstract`)); // relation incomplete from multiple level inheritance expect( - await loadModelWithError(` + await safelyLoadModel(` ${prelude} model User { id Int @id @default(autoincrement()) @@ -647,6 +696,6 @@ describe('Data Model Validation Tests', () => { a String } `) - ).toContain(`The relation field "user" on model "A" is missing an opposite relation field on model "User"`); + ).toMatchObject(errorLike(`The relation field "user" on model "A" is missing an opposite relation field on model "User"`)); }); }); diff --git a/packages/schema/tests/schema/validation/datasource-validation.test.ts b/packages/schema/tests/schema/validation/datasource-validation.test.ts index 19be1f076..469ba5ac1 100644 --- a/packages/schema/tests/schema/validation/datasource-validation.test.ts +++ b/packages/schema/tests/schema/validation/datasource-validation.test.ts @@ -1,18 +1,21 @@ -import { loadModel, loadModelWithError } from '../../utils'; +import { loadModel, loadModelWithError, safelyLoadModel } from '../../utils'; describe('Datasource Validation Tests', () => { it('missing fields', async () => { - expect( - await loadModelWithError(` + const result = await safelyLoadModel(` datasource db { } - `) - ).toEqual( - expect.arrayContaining([ - 'datasource must include a "provider" field', - 'datasource must include a "url" field', - ]) - ); + `); + + expect(result).toMatchObject({ + status: 'rejected', + reason: { + cause: [ + { message: 'datasource must include a "provider" field' }, + { message: 'datasource must include a "url" field' }, + ] + } + }) }); it('dup fields', async () => { @@ -41,7 +44,7 @@ describe('Datasource Validation Tests', () => { provider = 'abc' } `) - ).toContainEqual(expect.stringContaining('Provider "abc" is not supported')); + ).toContain('Provider "abc" is not supported'); }); it('invalid url value', async () => { diff --git a/packages/schema/tests/schema/validation/schema-validation.test.ts b/packages/schema/tests/schema/validation/schema-validation.test.ts index 5f1cc6254..ca0efa697 100644 --- a/packages/schema/tests/schema/validation/schema-validation.test.ts +++ b/packages/schema/tests/schema/validation/schema-validation.test.ts @@ -39,6 +39,20 @@ describe('Toplevel Schema Validation Tests', () => { ).toContain('Cannot find model file models/abc.zmodel'); }); + it('not existing import with extension', async () => { + expect( + await loadModelWithError(` + import 'models/abc.zmodel' + datasource db1 { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + model X {id String @id } + `) + ).toContain('Cannot find model file models/abc.zmodel'); + }) + it('multiple auth models', async () => { expect( await loadModelWithError(` diff --git a/packages/schema/tests/utils.ts b/packages/schema/tests/utils.ts index f88aae6e2..7369838f5 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/schema/tests/utils.ts @@ -7,9 +7,21 @@ import { URI } from 'vscode-uri'; import { createZModelServices } from '../src/language-server/zmodel-module'; import { mergeBaseModel } from '../src/utils/ast-utils'; -export class SchemaLoadingError extends Error { - constructor(public readonly errors: string[]) { - super('Schema error:\n' + errors.join('\n')); +type Errorish = Error | { message: string; stack?: string } | string; + +export class SchemaLoadingError extends Error { + cause: Errors + constructor(public readonly errors: Errors) { + const stack = errors.find((e): e is typeof e & { stack: string } => typeof e === 'object' && 'stack' in e)?.stack; + const message = errors.map((e) => (typeof e === 'string' ? e : e.message)).join('\n'); + + super(`Schema error:\n${ message }`); + + if (stack) { + const shiftedStack = stack.split('\n').slice(1).join('\n'); + this.stack = shiftedStack + } + this.cause = errors } } @@ -23,11 +35,11 @@ export async function loadModel(content: string, validate = true, verbose = true const doc = shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(docPath)); if (doc.parseResult.lexerErrors.length > 0) { - throw new SchemaLoadingError(doc.parseResult.lexerErrors.map((e) => e.message)); + throw new SchemaLoadingError(doc.parseResult.lexerErrors); } if (doc.parseResult.parserErrors.length > 0) { - throw new SchemaLoadingError(doc.parseResult.parserErrors.map((e) => e.message)); + throw new SchemaLoadingError(doc.parseResult.parserErrors); } await shared.workspace.DocumentBuilder.build([stdLib, doc], { @@ -46,7 +58,7 @@ export async function loadModel(content: string, validate = true, verbose = true ); } } - throw new SchemaLoadingError(validationErrors.map((e) => e.message)); + throw new SchemaLoadingError(validationErrors); } const model = (await doc.parseResult.value) as Model; @@ -65,7 +77,19 @@ export async function loadModelWithError(content: string, verbose = false) { if (!(err instanceof SchemaLoadingError)) { throw err; } - return (err as SchemaLoadingError).errors; + return (err as SchemaLoadingError).message; } throw new Error('No error is thrown'); } + +export async function safelyLoadModel(content: string, validate = true, verbose = false) { + const [ result ] = await Promise.allSettled([ loadModel(content, validate, verbose) ]); + + return result +} + +export const errorLike = (msg: string) => ({ + reason: { + message: expect.stringContaining(msg) + }, +}) diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 99d7ba495..d09ea775c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.9.0", + "version": "1.10.0", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 7ed1e3e29..37b7c4fd3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.9.0", + "version": "1.10.0", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", @@ -9,7 +9,7 @@ "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../.build'", "watch": "tsc --watch", "lint": "eslint src --ext ts", - "test": "ZENSTACK_TEST=1 jest", + "test": "jest", "prepublishOnly": "pnpm build" }, "publishConfig": { diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 7b084ef8a..7af912ec3 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -40,10 +40,11 @@ describe('REST server tests', () => { id Int @id @default(autoincrement()) createdAt DateTime @default (now()) updatedAt DateTime @updatedAt - title String + title String @length(1, 10) author User? @relation(fields: [authorId], references: [myId]) authorId String? published Boolean @default(false) + publishedAt DateTime? viewCount Int @default(0) comments Comment[] setting Setting? @@ -1293,6 +1294,49 @@ describe('REST server tests', () => { }); }); + it('creates an item with date coercion', async () => { + const r = await handler({ + method: 'post', + path: '/post', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { + id: 1, + title: 'Post1', + published: true, + publishedAt: '2024-03-02T05:00:00.000Z', + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(201); + }); + + it('creates an item with zod violation', async () => { + const r = await handler({ + method: 'post', + path: '/post', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { + id: 1, + title: 'a very very long long title', + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(400); + expect(r.body.errors[0].code).toBe('invalid-payload'); + }); + it('creates an item with collection relations', async () => { await prisma.post.create({ data: { id: 1, title: 'Post1' }, @@ -1586,6 +1630,50 @@ describe('REST server tests', () => { }); }); + it('update an item with date coercion', async () => { + await prisma.post.create({ data: { id: 1, title: 'Post1' } }); + + const r = await handler({ + method: 'put', + path: '/post/1', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { + published: true, + publishedAt: '2024-03-02T05:00:00.000Z', + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(200); + }); + + it('update an item with zod violation', async () => { + await prisma.post.create({ data: { id: 1, title: 'Post1' } }); + + const r = await handler({ + method: 'put', + path: '/post/1', + query: {}, + requestBody: { + data: { + type: 'post', + attributes: { + publishedAt: '2024-13-01', + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(400); + expect(r.body.errors[0].code).toBe('invalid-payload'); + }); + it('update a single relation', async () => { await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); await prisma.post.create({ diff --git a/packages/server/tests/utils.ts b/packages/server/tests/utils.ts index d1e0a0ffc..472a6818d 100644 --- a/packages/server/tests/utils.ts +++ b/packages/server/tests/utils.ts @@ -20,6 +20,7 @@ model Post { author User? @relation(fields: [authorId], references: [id]) authorId String? published Boolean @default(false) + publishedAt DateTime? viewCount Int @default(0) @@allow('all', author == auth()) diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 0aafe452e..1ee34f784 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.9.0", + "version": "1.10.0", "description": "ZenStack Test Tools", "main": "index.js", "private": true, @@ -11,7 +11,7 @@ "scripts": { "clean": "rimraf dist", "lint": "eslint src --ext ts", - "build": "pnpm lint && pnpm clean && tsc && copyfiles ./package.json ./LICENSE ./README.md dist && copyfiles -u 1 src/package.template.json src/.npmrc.template dist && pnpm pack dist --pack-destination '../../../.build'", + "build": "pnpm lint && pnpm clean && tsc && copyfiles ./package.json ./LICENSE ./README.md dist && pnpm pack dist --pack-destination '../../../.build'", "watch": "tsc --watch", "prepublishOnly": "pnpm build" }, diff --git a/packages/testtools/src/.npmrc.template b/packages/testtools/src/.npmrc.template deleted file mode 100644 index 14f2c2865..000000000 --- a/packages/testtools/src/.npmrc.template +++ /dev/null @@ -1 +0,0 @@ -cache=/.npmcache diff --git a/packages/testtools/src/package.template.json b/packages/testtools/src/package.template.json deleted file mode 100644 index 8ea542361..000000000 --- a/packages/testtools/src/package.template.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "test-run", - "version": "1.0.0", - "description": "", - "main": "index.js", - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@prisma/client": "^4.8.0", - "@zenstackhq/runtime": "file:/packages/runtime/dist", - "@zenstackhq/swr": "file:/packages/plugins/swr/dist", - "@zenstackhq/trpc": "file:/packages/plugins/trpc/dist", - "@zenstackhq/openapi": "file:/packages/plugins/openapi/dist", - "prisma": "^4.8.0", - "typescript": "^4.9.3", - "zenstack": "file:/packages/schema/dist", - "zod": "^3.22.4", - "decimal.js": "^10.4.2" - } -} diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index f69a845cc..c570c6a30 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -35,14 +35,23 @@ export type FullDbClientContract = Record & { }; export function run(cmd: string, env?: Record, cwd?: string) { - const start = Date.now(); - execSync(cmd, { - stdio: 'pipe', - encoding: 'utf-8', - env: { ...process.env, DO_NOT_TRACK: '1', ...env }, - cwd, - }); - console.log('Execution took', Date.now() - start, 'ms', '-', cmd); + try { + const start = Date.now(); + execSync(cmd, { + stdio: 'pipe', + encoding: 'utf-8', + env: { ...process.env, DO_NOT_TRACK: '1', ...env }, + cwd, + }); + console.log('Execution took', Date.now() - start, 'ms', '-', cmd); + } catch (err) { + console.error('Command failed:', cmd, err); + throw err; + } +} + +export function installPackage(pkg: string, dev = false) { + run(`npm install ${dev ? '-D' : ''} --no-audit --no-fund ${pkg}`); } function normalizePath(p: string) { @@ -86,17 +95,17 @@ generator js { plugin meta { provider = '@core/model-meta' - preserveTsFiles = true + // preserveTsFiles = true } plugin policy { provider = '@core/access-policy' - preserveTsFiles = true + // preserveTsFiles = true } plugin zod { provider = '@core/zod' - preserveTsFiles = true + // preserveTsFiles = true modelOnly = ${!options.fullZod} } `; @@ -138,21 +147,29 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { const { name: projectRoot } = tmp.dirSync({ unsafeCleanup: true }); - const root = getWorkspaceRoot(__dirname); + const workspaceRoot = getWorkspaceRoot(__dirname); - if (!root) { + if (!workspaceRoot) { throw new Error('Could not find workspace root'); } - const pkgContent = fs.readFileSync(path.join(__dirname, 'package.template.json'), { encoding: 'utf-8' }); - fs.writeFileSync(path.join(projectRoot, 'package.json'), pkgContent.replaceAll('', root)); - - const npmrcContent = fs.readFileSync(path.join(__dirname, '.npmrc.template'), { encoding: 'utf-8' }); - fs.writeFileSync(path.join(projectRoot, '.npmrc'), npmrcContent.replaceAll('', root)); - console.log('Workdir:', projectRoot); process.chdir(projectRoot); + // copy project structure from scaffold (prepared by test-setup.ts) + fs.cpSync(path.join(workspaceRoot, '.test/scaffold'), projectRoot, { recursive: true, force: true }); + + // install local deps + const localInstallDeps = [ + 'packages/schema/dist', + 'packages/runtime/dist', + 'packages/plugins/swr/dist', + 'packages/plugins/trpc/dist', + 'packages/plugins/openapi/dist', + ]; + + run(`npm i --no-audit --no-fund ${localInstallDeps.map((d) => path.join(workspaceRoot, d)).join(' ')}`); + let zmodelPath = path.join(projectRoot, 'schema.zmodel'); const files = schema.split(FILE_SPLITTER); @@ -189,16 +206,16 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { } } - run('npm install'); - const outputArg = opt.output ? ` --output ${opt.output}` : ''; if (opt.customSchemaFilePath) { - run(`npx zenstack generate --schema ${zmodelPath} --no-dependency-check${outputArg}`, { + run(`npx zenstack generate --no-version-check --schema ${zmodelPath} --no-dependency-check${outputArg}`, { NODE_PATH: './node_modules', }); } else { - run(`npx zenstack generate --no-dependency-check${outputArg}`, { NODE_PATH: './node_modules' }); + run(`npx zenstack generate --no-version-check --no-dependency-check${outputArg}`, { + NODE_PATH: './node_modules', + }); } if (opt.pushDb) { @@ -209,10 +226,10 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) { opt.extraDependencies?.push('@prisma/extension-pulse'); } - opt.extraDependencies?.forEach((dep) => { - console.log(`Installing dependency ${dep}`); - run(`npm install ${dep}`); - }); + if (opt.extraDependencies) { + console.log(`Installing dependency ${opt.extraDependencies.join(' ')}`); + installPackage(opt.extraDependencies.join(' ')); + } opt.copyDependencies?.forEach((dep) => { const pkgJson = JSON.parse(fs.readFileSync(path.join(dep, 'package.json'), { encoding: 'utf-8' })); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9dcef261b..b432b95c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: tsup: specifier: ^8.0.1 version: 8.0.1(ts-node@10.9.1)(typescript@5.3.2) + tsx: + specifier: ^4.7.1 + version: 4.7.1 typescript: specifier: ^5.3.2 version: 5.3.2 @@ -1594,6 +1597,15 @@ packages: tslib: 2.6.0 dev: true + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.17.19: resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} engines: {node: '>=12'} @@ -1612,6 +1624,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.19.4: resolution: {integrity: sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg==} engines: {node: '>=12'} @@ -1648,6 +1669,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.19.4: resolution: {integrity: sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ==} engines: {node: '>=12'} @@ -1675,6 +1705,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.19.4: resolution: {integrity: sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g==} engines: {node: '>=12'} @@ -1702,6 +1741,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.19.4: resolution: {integrity: sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA==} engines: {node: '>=12'} @@ -1729,6 +1777,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.19.4: resolution: {integrity: sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw==} engines: {node: '>=12'} @@ -1756,6 +1813,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.19.4: resolution: {integrity: sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ==} engines: {node: '>=12'} @@ -1783,6 +1849,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.19.4: resolution: {integrity: sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw==} engines: {node: '>=12'} @@ -1810,6 +1885,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.19.4: resolution: {integrity: sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA==} engines: {node: '>=12'} @@ -1837,6 +1921,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.19.4: resolution: {integrity: sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg==} engines: {node: '>=12'} @@ -1864,6 +1957,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.19.4: resolution: {integrity: sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ==} engines: {node: '>=12'} @@ -1900,6 +2002,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.19.4: resolution: {integrity: sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg==} engines: {node: '>=12'} @@ -1927,6 +2038,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.19.4: resolution: {integrity: sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw==} engines: {node: '>=12'} @@ -1954,6 +2074,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.19.4: resolution: {integrity: sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw==} engines: {node: '>=12'} @@ -1981,6 +2110,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.19.4: resolution: {integrity: sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig==} engines: {node: '>=12'} @@ -2008,6 +2146,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.19.4: resolution: {integrity: sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg==} engines: {node: '>=12'} @@ -2035,6 +2182,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.19.4: resolution: {integrity: sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg==} engines: {node: '>=12'} @@ -2062,6 +2218,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.19.4: resolution: {integrity: sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A==} engines: {node: '>=12'} @@ -2089,6 +2254,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.19.4: resolution: {integrity: sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw==} engines: {node: '>=12'} @@ -2116,6 +2290,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.19.4: resolution: {integrity: sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw==} engines: {node: '>=12'} @@ -2143,6 +2326,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.19.4: resolution: {integrity: sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w==} engines: {node: '>=12'} @@ -2170,6 +2362,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.19.4: resolution: {integrity: sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg==} engines: {node: '>=12'} @@ -2197,6 +2398,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.19.4: resolution: {integrity: sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA==} engines: {node: '>=12'} @@ -7770,6 +7980,37 @@ packages: '@esbuild/win32-x64': 0.18.14 dev: true + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + dev: true + /esbuild@0.19.4: resolution: {integrity: sha512-x7jL0tbRRpv4QUyuDMjONtWFciygUxWaUM1kMX2zWxI0X2YWOt7MSA0g4UdeSiHM8fcYVzpQhKYOycZwxTdZkA==} engines: {node: '>=12'} @@ -8597,6 +8838,12 @@ packages: get-intrinsic: 1.2.1 dev: true + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /giget@1.1.3: resolution: {integrity: sha512-zHuCeqtfgqgDwvXlR84UNgnJDuUHQcNI5OqWqFxxuk2BshuKbYhJWdxBsEo4PvKqoGh23lUAIvBNpChMLv7/9Q==} hasBin: true @@ -12958,6 +13205,10 @@ packages: engines: {node: '>=8'} dev: true + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve.exports@2.0.2: resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} engines: {node: '>=10'} @@ -14255,6 +14506,17 @@ packages: typescript: 5.3.2 dev: true + /tsx@4.7.1: + resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.19.12 + get-tsconfig: 4.7.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /tty-table@4.2.1: resolution: {integrity: sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==} engines: {node: '>=8.0.0'} diff --git a/script/set-test-env.ts b/script/set-test-env.ts new file mode 100644 index 000000000..4db61d8d1 --- /dev/null +++ b/script/set-test-env.ts @@ -0,0 +1 @@ +process.env.ZENSTACK_TEST = '1'; diff --git a/script/test-global-setup.ts b/script/test-global-setup.ts new file mode 100644 index 000000000..514cccae7 --- /dev/null +++ b/script/test-global-setup.ts @@ -0,0 +1,9 @@ +import fs from 'fs'; +import path from 'path'; + +export default function globalSetup() { + if (!fs.existsSync(path.join(__dirname, '../.test/scaffold/package-lock.json'))) { + console.error(`Test scaffold not found. Please run \`pnpm test-scaffold\` first.`); + process.exit(1); + } +} diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts new file mode 100644 index 000000000..ddf3c999a --- /dev/null +++ b/script/test-scaffold.ts @@ -0,0 +1,24 @@ +import path from 'path'; +import fs from 'fs'; +import { execSync } from 'child_process'; + +const scaffoldPath = path.join(__dirname, '../.test/scaffold'); +if (fs.existsSync(scaffoldPath)) { + fs.rmSync(scaffoldPath, { recursive: true, force: true }); +} +fs.mkdirSync(scaffoldPath, { recursive: true }); + +function run(cmd: string) { + console.log(`Running: ${cmd}, in ${scaffoldPath}`); + try { + execSync(cmd, { cwd: scaffoldPath, stdio: 'ignore' }); + } catch (err) { + console.error(`Test project scaffolding cmd error: ${err}`); + throw err; + } +} + +run('npm init -y'); +run('npm i --no-audit --no-fund typescript prisma @prisma/client zod decimal.js'); + +console.log('Test scaffold setup complete.'); diff --git a/tests/integration/global-setup.js b/tests/integration/global-setup.js deleted file mode 100644 index 0d4b8e23e..000000000 --- a/tests/integration/global-setup.js +++ /dev/null @@ -1,10 +0,0 @@ -const { execSync } = require('child_process'); - -module.exports = function () { - console.log('npm install'); - execSync('npm install', { - encoding: 'utf-8', - stdio: 'inherit', - cwd: 'test-run', - }); -}; diff --git a/tests/integration/jest.config.ts b/tests/integration/jest.config.ts index 346f6faad..67a118269 100644 --- a/tests/integration/jest.config.ts +++ b/tests/integration/jest.config.ts @@ -1,30 +1,10 @@ +import baseConfig from '../../jest.config'; + /* * For a detailed explanation regarding each configuration property and type check, visit: * https://jestjs.io/docs/configuration */ export default { - // Automatically clear mock calls, instances, contexts and results before every test - clearMocks: true, - - // A map from regular expressions to paths to transformers - transform: { '^.+\\.tsx?$': 'ts-jest' }, - - testTimeout: 300000, - + ...baseConfig, setupFilesAfterEnv: ['./test-setup.ts'], - - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, - - // The directory where Jest should output its coverage files - coverageDirectory: 'tests/coverage', - - // An array of regexp pattern strings used to skip coverage collection - coveragePathIgnorePatterns: ['/node_modules/', '/tests/'], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'v8', - - // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: ['json', 'text', 'lcov', 'clover'], }; diff --git a/tests/integration/package.json b/tests/integration/package.json index 40627f354..8aed0b6c8 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "lint": "eslint . --ext .ts", - "test": "ZENSTACK_TEST=1 jest" + "test": "jest" }, "keywords": [], "author": "", diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts index 0367033bd..90f9e2311 100644 --- a/tests/integration/tests/cli/generate.test.ts +++ b/tests/integration/tests/cli/generate.test.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-var-requires */ /// +import { installPackage } from '@zenstackhq/testtools'; import * as fs from 'fs'; import path from 'path'; import * as tmp from 'tmp'; import { createProgram } from '../../../../packages/schema/src/cli'; -import { execSync } from '../../../../packages/schema/src/utils/exec-utils'; import { createNpmrc } from './share'; describe('CLI generate command tests', () => { @@ -43,8 +43,8 @@ model Post { // set up project fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); createNpmrc(); - execSync('npm install prisma @prisma/client zod'); - execSync(`npm install ${path.join(__dirname, '../../../../packages/runtime/dist')}`); + installPackage('prisma @prisma/client zod'); + installPackage(path.join(__dirname, '../../../../packages/runtime/dist')); // set up schema fs.writeFileSync('schema.zmodel', MODEL, 'utf-8'); diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts index 005a0f69b..716ac224e 100644 --- a/tests/integration/tests/cli/plugins.test.ts +++ b/tests/integration/tests/cli/plugins.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ /// -import { getWorkspaceNpmCacheFolder, run } from '@zenstackhq/testtools'; +import { getWorkspaceNpmCacheFolder, installPackage, run } from '@zenstackhq/testtools'; import * as fs from 'fs'; import * as path from 'path'; import * as tmp from 'tmp'; @@ -94,8 +94,8 @@ describe('CLI Plugins Tests', () => { switch (pm) { case 'npm': - run('npm install ' + deps); - run('npm install -D ' + devDeps); + installPackage(deps); + installPackage(devDeps, true); break; // case 'yarn': // run('yarn add ' + deps); diff --git a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts index 55b9c5cee..bb505ca55 100644 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts @@ -45,7 +45,7 @@ describe('With Policy: field validation', () => { id String @id @default(cuid()) user User @relation(fields: [userId], references: [id]) userId String - slug String @regex("^[0-9a-zA-Z]{4,16}$") + slug String @regex("^[0-9a-zA-Z]{4,16}$") @lower @@allow('all', true) } @@ -508,50 +508,104 @@ describe('With Policy: field validation', () => { }, }); - await expect( - db.userData.create({ - data: { - userId: '1', - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - text6: ' AbC ', - text7: 'abc', + let ud = await db.userData.create({ + data: { + userId: '1', + a: 1, + b: 0, + c: -1, + d: 0, + text1: 'abc123', + text2: 'def', + text3: 'aaa', + text4: 'abcab', + text6: ' AbC ', + text7: 'abc', + }, + }); + expect(ud).toMatchObject({ text6: 'abc', text7: 'ABC' }); + + ud = await db.userData.update({ + where: { id: ud.id }, + data: { + text4: 'xyz', + text6: ' bCD ', + text7: 'bcd', + }, + }); + expect(ud).toMatchObject({ text4: 'xyz', text6: 'bcd', text7: 'BCD' }); + + let u = await db.user.create({ + data: { + id: '2', + password: 'abc123!@#', + email: 'who@myorg.com', + handle: 'user2', + userData: { + create: { + a: 1, + b: 0, + c: -1, + d: 0, + text1: 'abc123', + text2: 'def', + text3: 'aaa', + text4: 'abcab', + text6: ' AbC ', + text7: 'abc', + }, }, - }) - ).resolves.toMatchObject({ text6: 'abc', text7: 'ABC' }); + }, + include: { userData: true }, + }); + expect(u.userData).toMatchObject({ + text6: 'abc', + text7: 'ABC', + }); - await expect( - db.user.create({ - data: { - id: '2', - password: 'abc123!@#', - email: 'who@myorg.com', - handle: 'user2', - userData: { - create: { - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - text6: ' AbC ', - text7: 'abc', - }, + u = await db.user.update({ + where: { id: u.id }, + data: { + userData: { + update: { + data: { text4: 'xyz', text6: ' bCD ', text7: 'bcd' }, }, }, - include: { userData: true }, - }) - ).resolves.toMatchObject({ - userData: expect.objectContaining({ text6: 'abc', text7: 'ABC' }), + }, + include: { userData: true }, + }); + expect(u.userData).toMatchObject({ text4: 'xyz', text6: 'bcd', text7: 'BCD' }); + + // upsert create + u = await db.user.update({ + where: { id: u.id }, + data: { + tasks: { + upsert: { + where: { id: 'unknown' }, + create: { slug: 'SLUG1' }, + update: {}, + }, + }, + }, + include: { tasks: true }, + }); + expect(u.tasks[0]).toMatchObject({ slug: 'slug1' }); + + // upsert update + u = await db.user.update({ + where: { id: u.id }, + data: { + tasks: { + upsert: { + where: { id: u.tasks[0].id }, + create: {}, + update: { slug: 'SLUG2' }, + }, + }, + }, + include: { tasks: true }, }); + expect(u.tasks[0]).toMatchObject({ slug: 'slug2' }); }); }); diff --git a/tests/integration/tests/enhancements/with-policy/refactor.test.ts b/tests/integration/tests/enhancements/with-policy/refactor.test.ts index 126c038fa..6a329a739 100644 --- a/tests/integration/tests/enhancements/with-policy/refactor.test.ts +++ b/tests/integration/tests/enhancements/with-policy/refactor.test.ts @@ -144,12 +144,15 @@ describe('With Policy: refactor tests', () => { // read back check await expect( anonDb.user.create({ - data: { id: 1, email: 'user1@zenstack.dev' }, + data: { id: 1, email: 'User1@zenstack.dev' }, }) ).rejects.toThrow(/not allowed to be read back/); // success - await expect(user1Db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(user1Db.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ + // email to lower + email: 'user1@zenstack.dev', + }); // nested creation failure await expect( @@ -202,7 +205,7 @@ describe('With Policy: refactor tests', () => { posts: { create: { id: 2, - title: 'Post 2', + title: ' Post 2 ', published: true, comments: { create: { @@ -213,8 +216,14 @@ describe('With Policy: refactor tests', () => { }, }, }, + include: { posts: true }, }) - ).toResolveTruthy(); + ).resolves.toMatchObject({ + posts: expect.arrayContaining([ + // title is trimmed + expect.objectContaining({ title: 'Post 2' }), + ]), + }); // create with connect: posts await expect( @@ -389,7 +398,7 @@ describe('With Policy: refactor tests', () => { data: [ { id: 7, title: 'Post 7.1' }, { id: 7, title: 'Post 7.2' }, - { id: 8, title: 'Post 8' }, + { id: 8, title: ' Post 8 ' }, ], skipDuplicates: true, }, @@ -400,7 +409,10 @@ describe('With Policy: refactor tests', () => { // success await expect(adminDb.user.findUnique({ where: { id: 7 } })).toResolveTruthy(); await expect(adminDb.post.findUnique({ where: { id: 7 } })).toResolveTruthy(); - await expect(adminDb.post.findUnique({ where: { id: 8 } })).toResolveTruthy(); + await expect(adminDb.post.findUnique({ where: { id: 8 } })).resolves.toMatchObject({ + // title is trimmed + title: 'Post 8', + }); }); it('createMany', async () => { @@ -412,11 +424,18 @@ describe('With Policy: refactor tests', () => { await expect( user1Db.post.createMany({ data: [ - { id: 1, title: 'Post 1', authorId: 1 }, + { id: 1, title: ' Post 1 ', authorId: 1 }, { id: 2, title: 'Post 2', authorId: 1 }, ], }) - ).resolves.toMatchObject({ count: 2 }); + ).toResolveTruthy(); + + await expect(user1Db.post.findMany()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ title: 'Post 1' }), // title is trimmed + expect.objectContaining({ title: 'Post 2' }), + ]) + ); // unique constraint violation await expect( @@ -502,8 +521,8 @@ describe('With Policy: refactor tests', () => { user2Db.user.update({ where: { id: 1 }, data: { email: 'user2@zenstack.dev' } }) ).toBeRejectedByPolicy(); await expect( - adminDb.user.update({ where: { id: 1 }, data: { email: 'user1-nice@zenstack.dev' } }) - ).toResolveTruthy(); + adminDb.user.update({ where: { id: 1 }, data: { email: 'User1-nice@zenstack.dev' } }) + ).resolves.toMatchObject({ email: 'user1-nice@zenstack.dev' }); // update nested profile await expect( @@ -561,9 +580,10 @@ describe('With Policy: refactor tests', () => { await expect( user1Db.user.update({ where: { id: 1 }, - data: { posts: { update: { where: { id: 1 }, data: { published: false } } } }, + data: { posts: { update: { where: { id: 1 }, data: { title: ' New ', published: false } } } }, + include: { posts: true }, }) - ).toResolveTruthy(); + ).resolves.toMatchObject({ posts: expect.arrayContaining([expect.objectContaining({ title: 'New' })]) }); // update nested comment prevent update of toplevel await expect( @@ -588,23 +608,24 @@ describe('With Policy: refactor tests', () => { await expect(adminDb.comment.findFirst({ where: { content: 'Comment 2 updated' } })).toResolveFalsy(); // update with create - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - create: { - id: 3, - title: 'Post 3', - published: true, - comments: { - create: { author: { connect: { id: 1 } }, content: 'Comment 3' }, - }, + const r1 = await user1Db.user.update({ + where: { id: 1 }, + data: { + posts: { + create: { + id: 3, + title: 'Post 3', + published: true, + comments: { + create: { author: { connect: { id: 1 } }, content: ' Comment 3 ' }, }, }, }, - }) - ).toResolveTruthy(); + }, + include: { posts: { include: { comments: true } } }, + }); + expect(r1.posts[r1.posts.length - 1].comments[0].content).toEqual('Comment 3'); + await expect( user1Db.user.update({ where: { id: 1 }, @@ -636,7 +657,7 @@ describe('With Policy: refactor tests', () => { posts: { createMany: { data: [ - { id: 4, title: 'Post 4' }, + { id: 4, title: ' Post 4 ' }, { id: 5, title: 'Post 5' }, ], }, @@ -644,6 +665,7 @@ describe('With Policy: refactor tests', () => { }, }) ).toResolveTruthy(); + await expect(user1Db.post.findUnique({ where: { id: 4 } })).resolves.toMatchObject({ title: 'Post 4' }); await expect( user1Db.user.update({ include: { posts: true }, @@ -723,12 +745,13 @@ describe('With Policy: refactor tests', () => { posts: { update: { where: { id: 1 }, - data: { title: 'Post1-1' }, + data: { title: ' Post1-1' }, }, }, }, }) ).toResolveTruthy(); + await expect(user1Db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ title: 'Post1-1' }); await expect( user1Db.user.update({ where: { id: 1 }, @@ -799,14 +822,14 @@ describe('With Policy: refactor tests', () => { posts: { upsert: { where: { id: 1 }, - update: { title: 'Post 1-1' }, // update + update: { title: ' Post 2' }, // update create: { id: 7, title: 'Post 1' }, }, }, }, }) ).toResolveTruthy(); - await expect(user1Db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ title: 'Post 1-1' }); + await expect(user1Db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ title: 'Post 2' }); await expect( user1Db.user.update({ where: { id: 1 }, @@ -815,7 +838,7 @@ describe('With Policy: refactor tests', () => { upsert: { where: { id: 7 }, update: { title: 'Post 7-1' }, - create: { id: 7, title: 'Post 7' }, // create + create: { id: 7, title: ' Post 7' }, // create }, }, }, @@ -1094,9 +1117,10 @@ describe('With Policy: refactor tests', () => { ).toBeRejectedByPolicy(); await expect( user1Db.post.updateMany({ - data: { title: 'My post' }, + data: { title: ' My post' }, }) ).resolves.toMatchObject({ count: 2 }); + await expect(user1Db.post.findFirst()).resolves.toMatchObject({ title: 'My post' }); }); it('delete single', async () => { diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts index fd12d8b60..dd82f6786 100644 --- a/tests/integration/tests/plugins/zod.test.ts +++ b/tests/integration/tests/plugins/zod.test.ts @@ -503,6 +503,50 @@ describe('Zod plugin tests', () => { ).toBeFalsy(); }); + it('does date coercion', async () => { + const { zodSchemas } = await loadSchema( + ` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + generator js { + provider = 'prisma-client-js' + } + + plugin zod { + provider = "@core/zod" + } + + model Model { + id Int @id @default(autoincrement()) + dt DateTime + } + `, + { addPrelude: false, pushDb: false } + ); + const schemas = zodSchemas.models; + + expect( + schemas.ModelCreateSchema.safeParse({ + dt: new Date(), + }).success + ).toBeTruthy(); + + expect( + schemas.ModelCreateSchema.safeParse({ + dt: '2023-01-01T00:00:00.000Z', + }).success + ).toBeTruthy(); + + expect( + schemas.ModelCreateSchema.safeParse({ + dt: '2023-13-01', + }).success + ).toBeFalsy(); + }); + it('generate for selected models full', async () => { const { projectDir } = await loadSchema( ` diff --git a/tests/integration/tests/regression/issue-1014.test.ts b/tests/integration/tests/regression/issue-1014.test.ts new file mode 100644 index 000000000..ad862db42 --- /dev/null +++ b/tests/integration/tests/regression/issue-1014.test.ts @@ -0,0 +1,52 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1014', () => { + it('update', async () => { + const { prisma, enhance } = await loadSchema( + ` + model User { + id Int @id() @default(autoincrement()) + name String + posts Post[] + } + + model Post { + id Int @id() @default(autoincrement()) + title String + content String? + author User? @relation(fields: [authorId], references: [id]) + authorId Int? @allow('update', true, true) + + @@allow('read', true) + } + ` + ); + + const db = enhance(); + + const user = await prisma.user.create({ data: { name: 'User1' } }); + const post = await prisma.post.create({ data: { title: 'Post1' } }); + await expect(db.post.update({ where: { id: post.id }, data: { authorId: user.id } })).toResolveTruthy(); + }); + + it('read', async () => { + const { prisma, enhance } = await loadSchema( + ` + model Post { + id Int @id() @default(autoincrement()) + title String @allow('read', true, true) + content String + } + `, + { logPrismaQuery: true } + ); + + const db = enhance(); + + const post = await prisma.post.create({ data: { title: 'Post1', content: 'Content' } }); + await expect(db.post.findUnique({ where: { id: post.id } })).toResolveNull(); + await expect(db.post.findUnique({ where: { id: post.id }, select: { title: true } })).resolves.toEqual({ + title: 'Post1', + }); + }); +}); diff --git a/tests/integration/tests/regression/issue-177.test.ts b/tests/integration/tests/regression/issue-177.test.ts new file mode 100644 index 000000000..d270580c5 --- /dev/null +++ b/tests/integration/tests/regression/issue-177.test.ts @@ -0,0 +1,27 @@ +import { loadModelWithError } from '@zenstackhq/testtools'; + +describe('issue 177', () => { + it('regression', async () => { + await expect( + loadModelWithError( + ` + model Foo { + id String @id @default(cuid()) + + bar Bar @relation(fields: [barId1, barId2], references: [id1, id2]) + barId1 String? + barId2 String + } + + model Bar { + id1 String @default(cuid()) + id2 String @default(cuid()) + foos Foo[] + + @@id([id1, id2]) + } + ` + ) + ).resolves.toContain('relation "bar" is not optional, but field "barId1" is optional'); + }); +}); diff --git a/tests/integration/tests/schema/refactor-pg.zmodel b/tests/integration/tests/schema/refactor-pg.zmodel index f52f36c98..d0b4579e1 100644 --- a/tests/integration/tests/schema/refactor-pg.zmodel +++ b/tests/integration/tests/schema/refactor-pg.zmodel @@ -5,7 +5,7 @@ enum Role { model User { id Int @id @default(autoincrement()) - email String @unique @email + email String @unique @email @lower role Role @default(USER) profile Profile? posts Post[] @@ -52,7 +52,7 @@ model Image { model Post { id Int @id @default(autoincrement()) - title String @length(1, 8) + title String @length(1, 8) @trim published Boolean @default(false) comments Comment[] author User @relation(fields: [authorId], references: [id], onDelete: Cascade) @@ -67,7 +67,7 @@ model Post { model Comment { id Int @id @default(autoincrement()) - content String + content String @trim author User @relation(fields: [authorId], references: [id], onDelete: Cascade) authorId Int diff --git a/tests/integration/tests/schema/todo.zmodel b/tests/integration/tests/schema/todo.zmodel index 733391bd1..079e3b1ef 100644 --- a/tests/integration/tests/schema/todo.zmodel +++ b/tests/integration/tests/schema/todo.zmodel @@ -14,7 +14,7 @@ generator js { plugin zod { provider = '@core/zod' - preserveTsFiles = true + // preserveTsFiles = true } /* diff --git a/tests/integration/tests/tsconfig.template.json b/tests/integration/tests/tsconfig.template.json deleted file mode 100644 index 18a6bedec..000000000 --- a/tests/integration/tests/tsconfig.template.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - } -}