diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 884a2323..00000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -**/dist/** diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 049dbd47..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/no-unused-vars": [ - "error", - { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" } - ], - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/ban-ts-comment": "off" - } -} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..4af1219e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "tabWidth": 4, + "useTabs": false, + "printWidth": 120, + "singleQuote": true +} diff --git a/NEW-FEATURES.md b/NEW-FEATURES.md index 167ac913..b2aafc82 100644 --- a/NEW-FEATURES.md +++ b/NEW-FEATURES.md @@ -1,2 +1,2 @@ -- Cross-field comparison (for read and mutations) -- Computed fields +- Cross-field comparison (for read and mutations) +- Computed fields diff --git a/README.md b/README.md index 6e6ea9a4..8cc0fc1a 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ ZenStack is a TypeScript database toolkit for developing full-stack or backend Node.js/Bun applications. It provides a unified data modeling and access solution with the following features: -- A modern schema-first ORM that's compatible with [Prisma](https://github.com/prisma/prisma)'s schema and API -- Versatile data access APIs: high-level (Prisma-style) ORM queries + low-level ([Kysely](https://github.com/kysely-org/kysely)) query builder -- Built-in access control and data validation -- Advanced data modeling patterns like [polymorphism](https://zenstack.dev/blog/polymorphism) -- Designed for extensibility and flexibility: plugins, life-cycle hooks, etc. -- Automatic CRUD web APIs with adapters for popular frameworks -- Automatic [TanStack Query](https://github.com/TanStack/query) hooks for easy CRUD from the frontend +- A modern schema-first ORM that's compatible with [Prisma](https://github.com/prisma/prisma)'s schema and API +- Versatile data access APIs: high-level (Prisma-style) ORM queries + low-level ([Kysely](https://github.com/kysely-org/kysely)) query builder +- Built-in access control and data validation +- Advanced data modeling patterns like [polymorphism](https://zenstack.dev/blog/polymorphism) +- Designed for extensibility and flexibility: plugins, life-cycle hooks, etc. +- Automatic CRUD web APIs with adapters for popular frameworks +- Automatic [TanStack Query](https://github.com/TanStack/query) hooks for easy CRUD from the frontend # What's new with V3 @@ -83,10 +83,10 @@ Then create a `zenstack` folder and a `schema.zmodel` file in it. ZenStack uses a DSL named ZModel to model different aspects of database: -- Tables and fields -- Validation rules (coming soon) -- Access control policies (coming soon) -- ... +- Tables and fields +- Validation rules (coming soon) +- Access control policies (coming soon) +- ... ZModel is a super set of [Prisma Schema Language](https://www.prisma.io/docs/orm/prisma-schema/overview), i.e., every valid Prisma schema is a valid ZModel. @@ -288,9 +288,7 @@ client.$use({ async onQuery({ model, operation, proceed, queryArgs }) { const start = Date.now(); const result = await proceed(queryArgs); - console.log( - `[cost] ${model} ${operation} took ${Date.now() - start}ms` - ); + console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); return result; }, }); @@ -365,19 +363,19 @@ client.$use({ ZenStack v3 delegates database schema migration to Prisma. The CLI provides Prisma CLI wrappers for managing migrations. -- Sync schema to dev database and create a migration record: +- Sync schema to dev database and create a migration record: ```bash npx zenstack migrate dev ``` -- Deploy new migrations: +- Deploy new migrations: ```bash npx zenstack migrate deploy ``` -- Reset dev database +- Reset dev database ```bash npx zenstack migrate reset diff --git a/TODO.md b/TODO.md index fdc34f53..8f464ed5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,80 +1,80 @@ ## V3 Alpha Todo -- [ ] Infra - - [ ] Dependency injection -- [ ] CLI - - [x] generate - - [x] migrate - - [x] info - - [x] init -- [ ] ORM - - [x] Create - - [x] Input validation - - [x] Simple create - - [x] Nested create - - [x] Relation connection - - [x] Create many - - [x] ID generation - - [x] CreateManyAndReturn - - [x] Find - - [x] Input validation - - [x] Field selection - - [x] Omit - - [x] Counting relation - - [x] Pagination - - [x] Skip and limit - - [x] Cursor - - [x] Filtering - - [x] Unique fields - - [x] Scalar fields - - [x] Relation fields - - [x] Sort - - [x] Scalar fields - - [x] Relation fields - - [x] Relation inclusion - - [x] Filtering - - [x] Sorting - - [x] Pagination - - [x] Distinct - - [ ] JSON filtering - - [x] Update - - [x] Input validation - - [x] Top-level - - [x] Nested to-many - - [x] Nested to-one - - [x] Incremental update for numeric fields - - [x] Array update - - [x] Upsert - - [ ] Implement with "on conflict" - - [x] Delete - - [x] Aggregation - - [x] Count - - [x] Aggregate - - [x] Group by - - [ ] Extensions - - [x] Query builder API - - [x] Computed fields - - [ ] Prisma client extension - - [ ] Misc - - [x] JSDoc for CRUD methods - - [x] Cache validation schemas - - [x] Compound ID - - [ ] Cross field comparison - - [x] Many-to-many relation - - [ ] Empty AND/OR/NOT behavior - - [?] Logging - - [ ] Error system - - [x] Custom table name - - [x] Custom field name - - [ ] Implement changesets -- [ ] Polymorphism -- [ ] Validation -- [ ] Access Policy - - [ ] Short-circuit pre-create check for scalar-field only policies - - [ ] Inject "replace into" - - [ ] Inject "on conflict do update" -- [x] Migration -- [ ] Databases - - [x] SQLite - - [x] PostgreSQL - - [ ] Multi-schema +- [ ] Infra + - [ ] Dependency injection +- [ ] CLI + - [x] generate + - [x] migrate + - [x] info + - [x] init +- [ ] ORM + - [x] Create + - [x] Input validation + - [x] Simple create + - [x] Nested create + - [x] Relation connection + - [x] Create many + - [x] ID generation + - [x] CreateManyAndReturn + - [x] Find + - [x] Input validation + - [x] Field selection + - [x] Omit + - [x] Counting relation + - [x] Pagination + - [x] Skip and limit + - [x] Cursor + - [x] Filtering + - [x] Unique fields + - [x] Scalar fields + - [x] Relation fields + - [x] Sort + - [x] Scalar fields + - [x] Relation fields + - [x] Relation inclusion + - [x] Filtering + - [x] Sorting + - [x] Pagination + - [x] Distinct + - [ ] JSON filtering + - [x] Update + - [x] Input validation + - [x] Top-level + - [x] Nested to-many + - [x] Nested to-one + - [x] Incremental update for numeric fields + - [x] Array update + - [x] Upsert + - [ ] Implement with "on conflict" + - [x] Delete + - [x] Aggregation + - [x] Count + - [x] Aggregate + - [x] Group by + - [ ] Extensions + - [x] Query builder API + - [x] Computed fields + - [ ] Prisma client extension + - [ ] Misc + - [x] JSDoc for CRUD methods + - [x] Cache validation schemas + - [x] Compound ID + - [ ] Cross field comparison + - [x] Many-to-many relation + - [ ] Empty AND/OR/NOT behavior + - [?] Logging + - [ ] Error system + - [x] Custom table name + - [x] Custom field name + - [ ] Implement changesets +- [ ] Polymorphism +- [ ] Validation +- [ ] Access Policy + - [ ] Short-circuit pre-create check for scalar-field only policies + - [ ] Inject "replace into" + - [ ] Inject "on conflict do update" +- [x] Migration +- [ ] Databases + - [x] SQLite + - [x] PostgreSQL + - [ ] Multi-schema diff --git a/package.json b/package.json index 7c0e91e3..24888806 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "watch": "turbo run watch build", "lint": "turbo run lint", "test": "turbo run test", + "format": "prettier --write \"**/*.{ts,tsx,md}\"", "publish-all": "pnpm --filter \"./packages/**\" -r publish --tag next", "publish-preview": "pnpm --filter \"./packages/**\" -r publish --tag next --force --registry https://preview.registry.zenstack.dev/", "unpublish-preview": "pnpm --filter \"./packages/**\" -r --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ \"\\$PNPM_PACKAGE_NAME\"" @@ -16,16 +17,17 @@ "author": "", "license": "MIT", "devDependencies": { + "@eslint/js": "^9.29.0", "@swc/core": "^1.12.5", "@types/node": "^20.17.24", - "@typescript-eslint/eslint-plugin": "~7.3.1", - "@typescript-eslint/parser": "~7.3.1", - "eslint": "~8.57.1", + "eslint": "~9.29.0", "npm-run-all": "^4.1.5", + "prettier": "^3.5.3", "tsup": "^8.5.0", "tsx": "^4.20.3", "turbo": "^2.5.4", "typescript": "catalog:", + "typescript-eslint": "^8.34.1", "vitest": "^3.2.4" }, "pnpm": { diff --git a/packages/cli/eslint.config.js b/packages/cli/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/cli/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/cli/package.json b/packages/cli/package.json index 80fb473b..a63ca4d4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,6 +49,7 @@ "@zenstackhq/runtime": "workspace:*", "@zenstackhq/testtools": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*", "better-sqlite3": "^11.8.1", "tmp": "^0.2.3" } diff --git a/packages/cli/src/actions/action-utils.ts b/packages/cli/src/actions/action-utils.ts index 862b9d7c..2c736e50 100644 --- a/packages/cli/src/actions/action-utils.ts +++ b/packages/cli/src/actions/action-utils.ts @@ -17,7 +17,7 @@ export function getSchemaFile(file?: string) { return './schema.zmodel'; } else { throw new CliError( - 'Schema file not found in default locations ("./zenstack/schema.zmodel" or "./schema.zmodel").' + 'Schema file not found in default locations ("./zenstack/schema.zmodel" or "./schema.zmodel").', ); } } @@ -35,11 +35,7 @@ export async function loadSchemaDocument(schemaFile: string) { } export function handleSubProcessError(err: unknown) { - if ( - err instanceof Error && - 'status' in err && - typeof err.status === 'number' - ) { + if (err instanceof Error && 'status' in err && typeof err.status === 'number') { process.exit(err.status); } else { process.exit(1); diff --git a/packages/cli/src/actions/db.ts b/packages/cli/src/actions/db.ts index ffdb9c6e..cde7342e 100644 --- a/packages/cli/src/actions/db.ts +++ b/packages/cli/src/actions/db.ts @@ -20,10 +20,7 @@ export async function run(command: string, options: CommonOptions) { silent: true, }); - const prismaSchemaFile = path.join( - path.dirname(schemaFile), - 'schema.prisma' - ); + const prismaSchemaFile = path.join(path.dirname(schemaFile), 'schema.prisma'); switch (command) { case 'push': diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index c7c61adc..f377e2f3 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -48,18 +48,11 @@ const client = new ZenStackClient(schema, { } } -async function runPlugins( - model: Model, - outputPath: string, - tsSchemaFile: string -) { +async function runPlugins(model: Model, outputPath: string, tsSchemaFile: string) { const plugins = model.declarations.filter(isPlugin); for (const plugin of plugins) { const providerField = plugin.fields.find((f) => f.name === 'provider'); - invariant( - providerField, - `Plugin ${plugin.name} does not have a provider field` - ); + invariant(providerField, `Plugin ${plugin.name} does not have a provider field`); const provider = (providerField.value as LiteralExpr).value as string; let useProvider = provider; if (useProvider.startsWith('@core/')) { diff --git a/packages/cli/src/actions/info.ts b/packages/cli/src/actions/info.ts index 30584e6b..731bdaf8 100644 --- a/packages/cli/src/actions/info.ts +++ b/packages/cli/src/actions/info.ts @@ -7,9 +7,7 @@ import path from 'node:path'; export async function run(projectPath: string) { const packages = await getZenStackPackages(projectPath); if (!packages) { - console.error( - 'Unable to locate package.json. Are you in a valid project directory?' - ); + console.error('Unable to locate package.json. Are you in a valid project directory?'); return; } @@ -23,17 +21,11 @@ export async function run(projectPath: string) { } if (versions.size > 1) { - console.warn( - colors.yellow( - 'WARNING: Multiple versions of Zenstack packages detected. This may cause issues.' - ) - ); + console.warn(colors.yellow('WARNING: Multiple versions of Zenstack packages detected. This may cause issues.')); } } -async function getZenStackPackages( - projectPath: string -): Promise> { +async function getZenStackPackages(projectPath: string): Promise> { let pkgJson: { dependencies: Record; devDependencies: Record; @@ -45,17 +37,16 @@ async function getZenStackPackages( with: { type: 'json' }, }) ).default; - } catch (err) { + } catch { return []; } const packages = Array.from( new Set( - [ - ...Object.keys(pkgJson.dependencies ?? {}), - ...Object.keys(pkgJson.devDependencies ?? {}), - ].filter((p) => p.startsWith('@zenstackhq/') || p === 'zenstack') - ) + [...Object.keys(pkgJson.dependencies ?? {}), ...Object.keys(pkgJson.devDependencies ?? {})].filter( + (p) => p.startsWith('@zenstackhq/') || p === 'zenstack', + ), + ), ).sort(); const result = await Promise.all( @@ -70,7 +61,7 @@ async function getZenStackPackages( } catch { return { pkg, version: undefined }; } - }) + }), ); return result; diff --git a/packages/cli/src/actions/init.ts b/packages/cli/src/actions/init.ts index a2693f8e..6c63efda 100644 --- a/packages/cli/src/actions/init.ts +++ b/packages/cli/src/actions/init.ts @@ -28,9 +28,7 @@ export async function run(projectPath: string) { ...(pkg.dev ? [pm.agent === 'yarn' ? '--dev' : '--save-dev'] : []), ]); if (!resolved) { - throw new CliError( - `Unable to determine how to install package "${pkg.name}". Please install it manually.` - ); + throw new CliError(`Unable to determine how to install package "${pkg.name}". Please install it manually.`); } const spinner = ora(`Installing "${pkg.name}"`).start(); @@ -51,32 +49,13 @@ export async function run(projectPath: string) { fs.mkdirSync(path.join(projectPath, generationFolder)); } - if ( - !fs.existsSync( - path.join(projectPath, generationFolder, 'schema.zmodel') - ) - ) { - fs.writeFileSync( - path.join(projectPath, generationFolder, 'schema.zmodel'), - STARTER_ZMODEL - ); + if (!fs.existsSync(path.join(projectPath, generationFolder, 'schema.zmodel'))) { + fs.writeFileSync(path.join(projectPath, generationFolder, 'schema.zmodel'), STARTER_ZMODEL); } else { - console.log( - colors.yellow( - 'Schema file already exists. Skipping generation of sample.' - ) - ); + console.log(colors.yellow('Schema file already exists. Skipping generation of sample.')); } console.log(colors.green('ZenStack project initialized successfully!')); - console.log( - colors.gray( - `See "${generationFolder}/schema.zmodel" for your database schema.` - ) - ); - console.log( - colors.gray( - 'Run `zenstack generate` to compile the the schema into a TypeScript file.' - ) - ); + console.log(colors.gray(`See "${generationFolder}/schema.zmodel" for your database schema.`)); + console.log(colors.gray('Run `zenstack generate` to compile the the schema into a TypeScript file.')); } diff --git a/packages/cli/src/actions/migrate.ts b/packages/cli/src/actions/migrate.ts index 0c898404..fd0150c9 100644 --- a/packages/cli/src/actions/migrate.ts +++ b/packages/cli/src/actions/migrate.ts @@ -20,10 +20,7 @@ export async function run(command: string, options: CommonOptions) { silent: true, }); - const prismaSchemaFile = path.join( - path.dirname(schemaFile), - 'schema.prisma' - ); + const prismaSchemaFile = path.join(path.dirname(schemaFile), 'schema.prisma'); switch (command) { case 'dev': @@ -46,12 +43,9 @@ export async function run(command: string, options: CommonOptions) { async function runDev(prismaSchemaFile: string, _options: unknown) { try { - await execPackage( - `prisma migrate dev --schema "${prismaSchemaFile}" --skip-generate`, - { - stdio: 'inherit', - } - ); + await execPackage(`prisma migrate dev --schema "${prismaSchemaFile}" --skip-generate`, { + stdio: 'inherit', + }); } catch (err) { handleSubProcessError(err); } @@ -59,14 +53,9 @@ async function runDev(prismaSchemaFile: string, _options: unknown) { async function runReset(prismaSchemaFile: string, options: { force: boolean }) { try { - await execPackage( - `prisma migrate reset --schema "${prismaSchemaFile}"${ - options.force ? ' --force' : '' - }`, - { - stdio: 'inherit', - } - ); + await execPackage(`prisma migrate reset --schema "${prismaSchemaFile}"${options.force ? ' --force' : ''}`, { + stdio: 'inherit', + }); } catch (err) { handleSubProcessError(err); } @@ -74,12 +63,9 @@ async function runReset(prismaSchemaFile: string, options: { force: boolean }) { async function runDeploy(prismaSchemaFile: string, _options: unknown) { try { - await execPackage( - `prisma migrate deploy --schema "${prismaSchemaFile}"`, - { - stdio: 'inherit', - } - ); + await execPackage(`prisma migrate deploy --schema "${prismaSchemaFile}"`, { + stdio: 'inherit', + }); } catch (err) { handleSubProcessError(err); } @@ -87,23 +73,16 @@ async function runDeploy(prismaSchemaFile: string, _options: unknown) { async function runStatus(prismaSchemaFile: string, _options: unknown) { try { - await execPackage( - `prisma migrate status --schema "${prismaSchemaFile}"`, - { - stdio: 'inherit', - } - ); + await execPackage(`prisma migrate status --schema "${prismaSchemaFile}"`, { + stdio: 'inherit', + }); } catch (err) { handleSubProcessError(err); } } function handleSubProcessError(err: unknown) { - if ( - err instanceof Error && - 'status' in err && - typeof err.status === 'number' - ) { + if (err instanceof Error && 'status' in err && typeof err.status === 'number') { process.exit(err.status); } else { process.exit(1); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c15f412b..24c64b8b 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -4,9 +4,7 @@ import { Command, Option } from 'commander'; import * as actions from './actions'; import { getVersion } from './utils/version-utils'; -const generateAction = async ( - options: Parameters[0] -): Promise => { +const generateAction = async (options: Parameters[0]): Promise => { await actions.generate(options); }; @@ -36,60 +34,45 @@ export function createProgram() { program .description( `${colors.bold.blue( - 'ζ' - )} ZenStack is a Prisma power pack for building full-stack apps.\n\nDocumentation: https://zenstack.dev.` + 'ζ', + )} ZenStack is a Prisma power pack for building full-stack apps.\n\nDocumentation: https://zenstack.dev.`, ) .showHelpAfterError() .showSuggestionAfterError(); const schemaOption = new Option( '--schema ', - `schema file (with extension ${schemaExtensions}). Defaults to "schema.zmodel" unless specified in package.json.` + `schema file (with extension ${schemaExtensions}). Defaults to "schema.zmodel" unless specified in package.json.`, ); program .command('generate') .description('Run code generation.') .addOption(schemaOption) - .addOption( - new Option( - '-o, --output ', - 'default output directory for core plugins' - ) - ) + .addOption(new Option('-o, --output ', 'default output directory for core plugins')) .action(generateAction); - const migrateCommand = program - .command('migrate') - .description('Update the database schema with migrations.'); + const migrateCommand = program.command('migrate').description('Update the database schema with migrations.'); migrateCommand .command('dev') .addOption(schemaOption) .addOption(new Option('-n, --name ', 'migration name')) - .addOption( - new Option('--create-only', 'only create migration, do not apply') - ) - .description( - 'Create a migration from changes in schema and apply it to the database.' - ) + .addOption(new Option('--create-only', 'only create migration, do not apply')) + .description('Create a migration from changes in schema and apply it to the database.') .action((options) => migrateAction('dev', options)); migrateCommand .command('reset') .addOption(schemaOption) .addOption(new Option('--force', 'skip the confirmation prompt')) - .description( - 'Reset your database and apply all migrations, all data will be lost.' - ) + .description('Reset your database and apply all migrations, all data will be lost.') .action((options) => migrateAction('reset', options)); migrateCommand .command('deploy') .addOption(schemaOption) - .description( - 'Deploy your pending migrations to your production/staging database.' - ) + .description('Deploy your pending migrations to your production/staging database.') .action((options) => migrateAction('deploy', options)); migrateCommand @@ -98,30 +81,19 @@ export function createProgram() { .description('check the status of your database migrations.') .action((options) => migrateAction('status', options)); - const dbCommand = program - .command('db') - .description('Manage your database schema during development.'); + const dbCommand = program.command('db').description('Manage your database schema during development.'); dbCommand .command('push') .description('Push the state from your schema to your database') .addOption(schemaOption) - .addOption( - new Option('--accept-data-loss', 'ignore data loss warnings') - ) - .addOption( - new Option( - '--force-reset', - 'force a reset of the database before push' - ) - ) + .addOption(new Option('--accept-data-loss', 'ignore data loss warnings')) + .addOption(new Option('--force-reset', 'force a reset of the database before push')) .action((options) => dbAction('push', options)); program .command('info') - .description( - 'Get information of installed ZenStack and related packages.' - ) + .description('Get information of installed ZenStack and related packages.') .argument('[path]', 'project path', '.') .action(infoAction); diff --git a/packages/cli/src/utils/exec-utils.ts b/packages/cli/src/utils/exec-utils.ts index c4c215ee..4b2bd463 100644 --- a/packages/cli/src/utils/exec-utils.ts +++ b/packages/cli/src/utils/exec-utils.ts @@ -3,10 +3,7 @@ import { execSync as _exec, type ExecSyncOptions } from 'child_process'; /** * Utility for executing command synchronously and prints outputs on current console */ -export function execSync( - cmd: string, - options?: Omit & { env?: Record } -): void { +export function execSync(cmd: string, options?: Omit & { env?: Record }): void { const { env, ...restOptions } = options ?? {}; const mergedEnv = env ? { ...process.env, ...env } : undefined; _exec(cmd, { @@ -22,7 +19,7 @@ export function execSync( */ export function execPackage( cmd: string, - options?: Omit & { env?: Record } + options?: Omit & { env?: Record }, ): void { const packageManager = process?.versions?.['bun'] ? 'bunx' : 'npx'; execSync(`${packageManager} ${cmd}`, options); diff --git a/packages/cli/src/utils/version-utils.ts b/packages/cli/src/utils/version-utils.ts index 3a2daae5..31e7a107 100644 --- a/packages/cli/src/utils/version-utils.ts +++ b/packages/cli/src/utils/version-utils.ts @@ -1,13 +1,13 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -export function getVersion(): string | undefined { +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export function getVersion() { try { - return require('../package.json').version; + // isomorphic __dirname + const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + return JSON.parse(fs.readFileSync(path.join(_dirname, '../package.json'), 'utf8')).version; } catch { - try { - // dev environment - return require('../../package.json').version; - } catch { - return undefined; - } + return undefined; } } diff --git a/packages/create-zenstack/eslint.config.js b/packages/create-zenstack/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/create-zenstack/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index cc165c31..01505f38 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -35,6 +35,7 @@ "ora": "^5.4.1" }, "devDependencies": { - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*" } } diff --git a/packages/create-zenstack/src/index.ts b/packages/create-zenstack/src/index.ts index fb6db2f9..092a784e 100644 --- a/packages/create-zenstack/src/index.ts +++ b/packages/create-zenstack/src/index.ts @@ -57,8 +57,8 @@ function initProject(name: string) { license: 'ISC', }, null, - 2 - ) + 2, + ), ); // install packages @@ -92,8 +92,8 @@ function initProject(name: string) { }, }, null, - 2 - ) + 2, + ), ); // create schema.zmodel @@ -111,10 +111,7 @@ function initProject(name: string) { } function installPackage(pkg: { name: string; dev: boolean }) { - runCommand( - `${agent} install ${pkg.name} ${pkg.dev ? saveDev : ''}`, - `Installing "${pkg.name}"` - ); + runCommand(`${agent} install ${pkg.name} ${pkg.dev ? saveDev : ''}`, `Installing "${pkg.name}"`); } function runCommand(cmd: string, status: string) { diff --git a/packages/eslint-config/base.js b/packages/eslint-config/base.js new file mode 100644 index 00000000..b87c0543 --- /dev/null +++ b/packages/eslint-config/base.js @@ -0,0 +1,17 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +/** + * A shared ESLint configuration for the repository. + * + * @type {import("eslint").Linter.Config[]} + * */ +export default tseslint.config(eslint.configs.recommended, tseslint.configs.recommended, { + rules: { + '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + }, +}); diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json new file mode 100644 index 00000000..e54751ad --- /dev/null +++ b/packages/eslint-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "@zenstackhq/eslint-config", + "version": "3.0.0-alpha.4", + "type": "module", + "private": true, + "license": "MIT" +} diff --git a/packages/ide/vscode/README.md b/packages/ide/vscode/README.md index cfb38c25..44d12fd1 100644 --- a/packages/ide/vscode/README.md +++ b/packages/ide/vscode/README.md @@ -6,9 +6,9 @@ This VS Code extension provides code editing helpers for authoring ZenStack's sc ## Features -- Syntax highlighting of `*.zmodel` files +- Syntax highlighting of `*.zmodel` files - - In case the schema file is not recognized automatically, add the following to your settings.json file: + - In case the schema file is not recognized automatically, add the following to your settings.json file: ```json "files.associations": { @@ -16,33 +16,33 @@ This VS Code extension provides code editing helpers for authoring ZenStack's sc }, ``` -- Auto formatting +- Auto formatting - - To automatically format on save, add the following to your settings.json file: + - To automatically format on save, add the following to your settings.json file: ```json "editor.formatOnSave": true ``` - - To enable formatting in combination with prettier, add the following to your settings.json file: + - To enable formatting in combination with prettier, add the following to your settings.json file: ```json "[zmodel]": { "editor.defaultFormatter": "zenstack.zenstack" }, ``` -- Inline error reporting -- Go-to definition -- Hover documentation -- Code section folding +- Inline error reporting +- Go-to definition +- Hover documentation +- Code section folding ## Links -- [Home](https://zenstack.dev) -- [Documentation](https://zenstack.dev/docs) -- [Community chat](https://discord.gg/Ykhr738dUe) -- [Twitter](https://twitter.com/zenstackhq) -- [Blog](https://dev.to/zenstack) +- [Home](https://zenstack.dev) +- [Documentation](https://zenstack.dev/docs) +- [Community chat](https://discord.gg/Ykhr738dUe) +- [Twitter](https://twitter.com/zenstackhq) +- [Blog](https://dev.to/zenstack) ## Community diff --git a/packages/ide/vscode/eslint.config.mjs b/packages/ide/vscode/eslint.config.mjs new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/ide/vscode/eslint.config.mjs @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 277f054c..4bfd341f 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -37,7 +37,8 @@ }, "devDependencies": { "@types/vscode": "^1.63.0", - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*" }, "files": [ "dist", diff --git a/packages/ide/vscode/src/extension/main.ts b/packages/ide/vscode/src/extension/main.ts index 189eecdb..fed30b20 100644 --- a/packages/ide/vscode/src/extension/main.ts +++ b/packages/ide/vscode/src/extension/main.ts @@ -1,9 +1,6 @@ import * as path from 'node:path'; import type * as vscode from 'vscode'; -import type { - LanguageClientOptions, - ServerOptions, -} from 'vscode-languageclient/node.js'; +import type { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node.js'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node.js'; let client: LanguageClient; @@ -22,18 +19,14 @@ export function deactivate(): Thenable | undefined { } function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { - const serverModule = context.asAbsolutePath( - path.join('dist', 'language-server.js') - ); + const serverModule = context.asAbsolutePath(path.join('dist', 'language-server.js')); // The debug options for the server // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging. // By setting `process.env.DEBUG_BREAK` to a truthy value, the language server will wait until a debugger is attached. const debugOptions = { execArgv: [ '--nolazy', - `--inspect${process.env['DEBUG_BREAK'] ? '-brk' : ''}=${ - process.env['DEBUG_SOCKET'] || '6009' - }`, + `--inspect${process.env['DEBUG_BREAK'] ? '-brk' : ''}=${process.env['DEBUG_SOCKET'] || '6009'}`, ], }; @@ -54,12 +47,7 @@ function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { }; // Create the language client and start the client. - const client = new LanguageClient( - 'zmodel', - 'ZModel', - serverOptions, - clientOptions - ); + const client = new LanguageClient('zmodel', 'ZModel', serverOptions, clientOptions); // Start the client. This will also launch the server client.start(); diff --git a/packages/ide/vscode/src/language-server/main.ts b/packages/ide/vscode/src/language-server/main.ts index 6bd10e9e..b9ac6998 100644 --- a/packages/ide/vscode/src/language-server/main.ts +++ b/packages/ide/vscode/src/language-server/main.ts @@ -1,10 +1,7 @@ import { createZModelLanguageServices } from '@zenstackhq/language'; import { startLanguageServer } from 'langium/lsp'; import { NodeFileSystem } from 'langium/node'; -import { - createConnection, - ProposedFeatures, -} from 'vscode-languageserver/node.js'; +import { createConnection, ProposedFeatures } from 'vscode-languageserver/node.js'; // Create a connection to the client const connection = createConnection(ProposedFeatures.all); diff --git a/packages/language/eslint.config.js b/packages/language/eslint.config.js new file mode 100644 index 00000000..94f3c8b8 --- /dev/null +++ b/packages/language/eslint.config.js @@ -0,0 +1,7 @@ +import config from '@zenstackhq/eslint-config/base.js'; +import tseslint from 'typescript-eslint'; + +/** @type {import("eslint").Linter.Config} */ +export default tseslint.config(config, { + ignores: ['src/generated/**/*'], +}); diff --git a/packages/language/langium-quickstart.md b/packages/language/langium-quickstart.md deleted file mode 100644 index a81ba485..00000000 --- a/packages/language/langium-quickstart.md +++ /dev/null @@ -1,40 +0,0 @@ -# Welcome to your Langium VS Code Extension - -## What's in the folder - -This folder contains all necessary files for your language extension. - * `package.json` - the manifest file in which you declare your language support. - * `language-configuration.json` - the language configuration used in the VS Code editor, defining the tokens that are used for comments and brackets. - * `src/extension/main.ts` - the main code of the extension, which is responsible for launching a language server and client. - * `src/language/z-model-language.langium` - the grammar definition of your language. - * `src/language/main.ts` - the entry point of the language server process. - * `src/language/z-model-language-module.ts` - the dependency injection module of your language implementation. Use this to register overridden and added services. - * `src/language/z-model-language-validator.ts` - an example validator. You should change it to reflect the semantics of your language. - * `src/cli/main.ts` - the entry point of the command line interface (CLI) of your language. - * `src/cli/generator.ts` - the code generator used by the CLI to write output files from DSL documents. - * `src/cli/cli-util.ts` - utility code for the CLI. - -## Get up and running straight away - - * Run `npm run langium:generate` to generate TypeScript code from the grammar definition. - * Run `npm run build` to compile all TypeScript code. - * Press `F5` to open a new window with your extension loaded. - * Create a new file with a file name suffix matching your language. - * Verify that syntax highlighting, validation, completion etc. are working as expected. - * Run `node ./bin/cli` to see options for the CLI; `node ./bin/cli generate ` generates code for a given DSL file. - -## Make changes - - * Run `npm run watch` to have the TypeScript compiler run automatically after every change of the source files. - * Run `npm run langium:watch` to have the Langium generator run automatically after every change of the grammar declaration. - * You can relaunch the extension from the debug toolbar after making changes to the files listed above. - * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. - -## Install your extension - -* To start using your extension with VS Code, copy it into the `/.vscode/extensions` folder and restart Code. -* To share your extension with the world, read the [VS Code documentation](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) about publishing an extension. - -## To Go Further - -Documentation about the Langium framework is available at https://langium.org diff --git a/packages/language/package.json b/packages/language/package.json index 1bd50d40..5555b630 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -50,7 +50,8 @@ "devDependencies": { "@types/pluralize": "^0.0.33", "langium-cli": "~3.3.0", - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*" }, "volta": { "node": "18.19.1", diff --git a/packages/language/src/ast.ts b/packages/language/src/ast.ts index e0ad435f..4ba343b7 100644 --- a/packages/language/src/ast.ts +++ b/packages/language/src/ast.ts @@ -1,10 +1,5 @@ import type { AstNode } from 'langium'; -import { - AbstractDeclaration, - BinaryExpr, - DataModel, - type ExpressionType, -} from './generated/ast'; +import { AbstractDeclaration, BinaryExpr, DataModel, type ExpressionType } from './generated/ast'; export type { AstNode, Reference } from 'langium'; export * from './generated/ast'; @@ -23,10 +18,7 @@ export type ResolvedType = { nullable?: boolean; }; -export const BinaryExprOperatorPriority: Record< - BinaryExpr['operator'], - number -> = { +export const BinaryExprOperatorPriority: Record = { //LogicalExpr '||': 1, '&&': 1, diff --git a/packages/language/src/constants.ts b/packages/language/src/constants.ts index 843fd338..973880a4 100644 --- a/packages/language/src/constants.ts +++ b/packages/language/src/constants.ts @@ -13,16 +13,7 @@ export const SUPPORTED_PROVIDERS = [ /** * All scalar types */ -export const SCALAR_TYPES = [ - 'String', - 'Int', - 'Float', - 'Decimal', - 'BigInt', - 'Boolean', - 'Bytes', - 'DateTime', -]; +export const SCALAR_TYPES = ['String', 'Int', 'Float', 'Decimal', 'BigInt', 'Boolean', 'Bytes', 'DateTime']; /** * Name of standard library module diff --git a/packages/language/src/index.ts b/packages/language/src/index.ts index 102a04f9..3c9b23f5 100644 --- a/packages/language/src/index.ts +++ b/packages/language/src/index.ts @@ -19,10 +19,9 @@ export class DocumentLoadError extends Error { export async function loadDocument( fileName: string, - pluginModelFiles: string[] = [] + pluginModelFiles: string[] = [], ): Promise< - | { success: true; model: Model; warnings: string[] } - | { success: false; errors: string[]; warnings: string[] } + { success: true; model: Model; warnings: string[] } | { success: false; errors: string[]; warnings: string[] } > { const { ZModelLanguage: services } = createZModelServices(); const extensions = services.LanguageMetaData.fileExtensions; @@ -45,44 +44,29 @@ export async function loadDocument( // load standard library // isomorphic __dirname - const _dirname = - typeof __dirname !== 'undefined' - ? __dirname - : path.dirname(fileURLToPath(import.meta.url)); - const stdLib = - await services.shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file( - path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME)) - ) - ); + const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); + const stdLib = await services.shared.workspace.LangiumDocuments.getOrCreateDocument( + URI.file(path.resolve(path.join(_dirname, '../res', STD_LIB_MODULE_NAME))), + ); // load plugin model files const pluginDocs = await Promise.all( pluginModelFiles.map((file) => - services.shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file(path.resolve(file)) - ) - ) + services.shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(path.resolve(file))), + ), ); // load the document const langiumDocuments = services.shared.workspace.LangiumDocuments; - const document = await langiumDocuments.getOrCreateDocument( - URI.file(path.resolve(fileName)) - ); + const document = await langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName))); // build the document together with standard library, plugin modules, and imported documents - await services.shared.workspace.DocumentBuilder.build( - [stdLib, ...pluginDocs, document], - { - validation: true, - } - ); + await services.shared.workspace.DocumentBuilder.build([stdLib, ...pluginDocs, document], { + validation: true, + }); const diagnostics = langiumDocuments.all - .flatMap((doc) => - (doc.diagnostics ?? []).map((diag) => ({ doc, diag })) - ) + .flatMap((doc) => (doc.diagnostics ?? []).map((diag) => ({ doc, diag }))) .filter(({ diag }) => diag.severity === 1 || diag.severity === 2) .toArray(); diff --git a/packages/language/src/module.ts b/packages/language/src/module.ts index 0721285b..00e54985 100644 --- a/packages/language/src/module.ts +++ b/packages/language/src/module.ts @@ -7,11 +7,7 @@ import { type LangiumSharedServices, type PartialLangiumServices, } from 'langium/lsp'; -import { - ZModelGeneratedModule, - ZModelGeneratedSharedModule, - ZModelLanguageMetaData, -} from './generated/module'; +import { ZModelGeneratedModule, ZModelGeneratedSharedModule, ZModelLanguageMetaData } from './generated/module'; import { ZModelValidator, registerValidationChecks } from './validator'; import { ZModelLinker } from './zmodel-linker'; import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope'; @@ -38,10 +34,7 @@ export type ZModelServices = LangiumServices & ZModelAddedServices; * declared custom services. The Langium defaults can be partially specified to override only * selected services, while the custom services must be fully specified. */ -export const ZModelLanguageModule: Module< - ZModelServices, - PartialLangiumServices & ZModelAddedServices -> = { +export const ZModelLanguageModule: Module = { references: { ScopeComputation: (services) => new ZModelScopeComputation(services), ScopeProvider: (services) => new ZModelScopeProvider(services), @@ -54,10 +47,7 @@ export const ZModelLanguageModule: Module< export type ZModelSharedServices = LangiumSharedServices; -export const ZModelSharedModule: Module< - ZModelSharedServices, - DeepPartial -> = { +export const ZModelSharedModule: Module> = { workspace: { WorkspaceManager: (services) => new ZModelWorkspaceManager(services), }, @@ -78,22 +68,12 @@ export const ZModelSharedModule: Module< * @param context Optional module context with the LSP connection * @returns An object wrapping the shared services and the language-specific services */ -export function createZModelLanguageServices( - context: DefaultSharedModuleContext -): { +export function createZModelLanguageServices(context: DefaultSharedModuleContext): { shared: LangiumSharedServices; ZModelLanguage: ZModelServices; } { - const shared = inject( - createDefaultSharedModule(context), - ZModelGeneratedSharedModule, - ZModelSharedModule - ); - const ZModelLanguage = inject( - createDefaultModule({ shared }), - ZModelGeneratedModule, - ZModelLanguageModule - ); + const shared = inject(createDefaultSharedModule(context), ZModelGeneratedSharedModule, ZModelSharedModule); + const ZModelLanguage = inject(createDefaultModule({ shared }), ZModelGeneratedModule, ZModelLanguageModule); shared.ServiceRegistry.register(ZModelLanguage); registerValidationChecks(ZModelLanguage); if (!context.connection) { diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index 39353803..be6b3c67 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -1,10 +1,4 @@ -import { - AstUtils, - URI, - type AstNode, - type LangiumDocuments, - type Reference, -} from 'langium'; +import { AstUtils, URI, type AstNode, type LangiumDocuments, type Reference } from 'langium'; import path from 'path'; import { STD_LIB_MODULE_NAME, type ExpressionContext } from './constants'; import { @@ -62,55 +56,35 @@ export function hasAttribute(decl: AttributeTarget, name: string) { } export function getAttribute(decl: AttributeTarget, name: string) { - return ( - decl.attributes as (DataModelAttribute | DataModelFieldAttribute)[] - ).find((attr) => attr.decl.$refText === name); + return (decl.attributes as (DataModelAttribute | DataModelFieldAttribute)[]).find( + (attr) => attr.decl.$refText === name, + ); } export function isFromStdlib(node: AstNode) { const model = AstUtils.getContainerOfType(node, isModel); - return ( - !!model && - !!model.$document && - model.$document.uri.path.endsWith(STD_LIB_MODULE_NAME) - ); + return !!model && !!model.$document && model.$document.uri.path.endsWith(STD_LIB_MODULE_NAME); } export function isAuthInvocation(node: AstNode) { - return ( - isInvocationExpr(node) && - node.function.ref?.name === 'auth' && - isFromStdlib(node.function.ref) - ); + return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); } /** * Try getting string value from a potential string literal expression */ -export function getStringLiteral( - node: AstNode | undefined -): string | undefined { +export function getStringLiteral(node: AstNode | undefined): string | undefined { return isStringLiteral(node) ? node.value : undefined; } -const isoDateTimeRegex = - /^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i; +const isoDateTimeRegex = /^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i; /** * Determines if the given sourceType is assignable to a destination of destType */ -export function typeAssignable( - destType: ExpressionType, - sourceType: ExpressionType, - sourceExpr?: Expression -): boolean { +export function typeAssignable(destType: ExpressionType, sourceType: ExpressionType, sourceExpr?: Expression): boolean { // implicit conversion from ISO datetime string to datetime - if ( - destType === 'DateTime' && - sourceType === 'String' && - sourceExpr && - isStringLiteral(sourceExpr) - ) { + if (destType === 'DateTime' && sourceType === 'String' && sourceExpr && isStringLiteral(sourceExpr)) { const literal = getStringLiteral(sourceExpr); if (literal && isoDateTimeRegex.test(literal)) { // implicitly convert to DateTime @@ -122,11 +96,7 @@ export function typeAssignable( case 'Any': return true; case 'Float': - return ( - sourceType === 'Any' || - sourceType === 'Int' || - sourceType === 'Float' - ); + return sourceType === 'Any' || sourceType === 'Int' || sourceType === 'Float'; default: return sourceType === 'Any' || sourceType === destType; } @@ -136,7 +106,7 @@ export function typeAssignable( * Maps a ZModel builtin type to expression type */ export function mapBuiltinTypeToExpressionType( - type: BuiltinType | 'Any' | 'Object' | 'Null' | 'Unsupported' + type: BuiltinType | 'Any' | 'Object' | 'Null' | 'Unsupported', ): ExpressionType | 'Any' { switch (type) { case 'Any': @@ -162,19 +132,14 @@ export function mapBuiltinTypeToExpressionType( } export function isAuthOrAuthMemberAccess(expr: Expression): boolean { - return ( - isAuthInvocation(expr) || - (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand)) - ); + return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand)); } export function isEnumFieldReference(node: AstNode): node is ReferenceExpr { return isReferenceExpr(node) && isEnumField(node.target.ref); } -export function isDataModelFieldReference( - node: AstNode -): node is ReferenceExpr { +export function isDataModelFieldReference(node: AstNode): node is ReferenceExpr { return isReferenceExpr(node) && isDataModelField(node.target.ref); } @@ -186,11 +151,7 @@ export function isRelationshipField(field: DataModelField) { } export function isFutureExpr(node: AstNode) { - return ( - isInvocationExpr(node) && - node.function.ref?.name === 'future' && - isFromStdlib(node.function.ref) - ); + return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref); } export function isDelegateModel(node: AstNode) { @@ -207,10 +168,7 @@ export function resolved(ref: Reference): T { /** * Walk up the inheritance chain to find the path from the start model to the target model */ -export function findUpInheritance( - start: DataModel, - target: DataModel -): DataModel[] | undefined { +export function findUpInheritance(start: DataModel, target: DataModel): DataModel[] | undefined { for (const base of start.superTypes) { if (base.ref === target) { return [base.ref]; @@ -223,26 +181,18 @@ export function findUpInheritance( return undefined; } -export function getModelFieldsWithBases( - model: DataModel, - includeDelegate = true -) { +export function getModelFieldsWithBases(model: DataModel, includeDelegate = true) { if (model.$baseMerged) { return model.fields; } else { - return [ - ...model.fields, - ...getRecursiveBases(model, includeDelegate).flatMap( - (base) => base.fields - ), - ]; + return [...model.fields, ...getRecursiveBases(model, includeDelegate).flatMap((base) => base.fields)]; } } export function getRecursiveBases( dataModel: DataModel, includeDelegate = true, - seen = new Set() + seen = new Set(), ): DataModel[] { const result: DataModel[] = []; if (seen.has(dataModel)) { @@ -266,20 +216,14 @@ export function getRecursiveBases( * Gets `@@id` fields declared at the data model level (including search in base models) */ export function getModelIdFields(model: DataModel) { - const modelsToCheck = model.$baseMerged - ? [model] - : [model, ...getRecursiveBases(model)]; + const modelsToCheck = model.$baseMerged ? [model] : [model, ...getRecursiveBases(model)]; for (const modelToCheck of modelsToCheck) { - const idAttr = modelToCheck.attributes.find( - (attr) => attr.decl.$refText === '@@id' - ); + const idAttr = modelToCheck.attributes.find((attr) => attr.decl.$refText === '@@id'); if (!idAttr) { continue; } - const fieldsArg = idAttr.args.find( - (a) => a.$resolvedParam?.name === 'fields' - ); + const fieldsArg = idAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { continue; } @@ -296,20 +240,14 @@ export function getModelIdFields(model: DataModel) { * Gets `@@unique` fields declared at the data model level (including search in base models) */ export function getModelUniqueFields(model: DataModel) { - const modelsToCheck = model.$baseMerged - ? [model] - : [model, ...getRecursiveBases(model)]; + const modelsToCheck = model.$baseMerged ? [model] : [model, ...getRecursiveBases(model)]; for (const modelToCheck of modelsToCheck) { - const uniqueAttr = modelToCheck.attributes.find( - (attr) => attr.decl.$refText === '@@unique' - ); + const uniqueAttr = modelToCheck.attributes.find((attr) => attr.decl.$refText === '@@unique'); if (!uniqueAttr) { continue; } - const fieldsArg = uniqueAttr.args.find( - (a) => a.$resolvedParam?.name === 'fields' - ); + const fieldsArg = uniqueAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { continue; } @@ -329,13 +267,10 @@ export function getModelUniqueFields(model: DataModel) { */ export function getUniqueFields(model: DataModel) { const uniqueAttrs = model.attributes.filter( - (attr) => - attr.decl.ref?.name === '@@unique' || attr.decl.ref?.name === '@@id' + (attr) => attr.decl.ref?.name === '@@unique' || attr.decl.ref?.name === '@@id', ); return uniqueAttrs.map((uniqueAttr) => { - const fieldsArg = uniqueAttr.args.find( - (a) => a.$resolvedParam?.name === 'fields' - ); + const fieldsArg = uniqueAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { return []; } @@ -346,10 +281,7 @@ export function getUniqueFields(model: DataModel) { }); } -export function findUpAst( - node: AstNode, - predicate: (node: AstNode) => boolean -): AstNode | undefined { +export function findUpAst(node: AstNode, predicate: (node: AstNode) => boolean): AstNode | undefined { let curr: AstNode | undefined = node; while (curr) { if (predicate(curr)) { @@ -361,7 +293,7 @@ export function findUpAst( } export function getLiteral( - expr: Expression | ConfigExpr | undefined + expr: Expression | ConfigExpr | undefined, ): T | undefined { switch (expr?.$type) { case 'ObjectExpr': @@ -376,9 +308,7 @@ export function getLiteral( } } -export function getObjectLiteral( - expr: Expression | ConfigExpr | undefined -): T | undefined { +export function getObjectLiteral(expr: Expression | ConfigExpr | undefined): T | undefined { if (!expr || !isObjectExpr(expr)) { return undefined; } @@ -402,27 +332,23 @@ export function getObjectLiteral( } export function getLiteralArray< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends string | number | boolean | any = any + + T extends string | number | boolean | any = any, >(expr: Expression | ConfigExpr | undefined): T[] | undefined { const arr = getArray(expr); if (!arr) { return undefined; } - return arr - .map((item) => isExpression(item) && getLiteral(item)) - .filter((v): v is T => v !== undefined); + return arr.map((item) => isExpression(item) && getLiteral(item)).filter((v): v is T => v !== undefined); } function getArray(expr: Expression | ConfigExpr | undefined) { - return isArrayExpr(expr) || isConfigArrayExpr(expr) - ? expr.items - : undefined; + return isArrayExpr(expr) || isConfigArrayExpr(expr) ? expr.items : undefined; } export function getAttributeArgLiteral( attr: DataModelAttribute | DataModelFieldAttribute, - name: string + name: string, ): T | undefined { for (const arg of attr.args) { if (arg.$resolvedParam?.name === name) { @@ -434,17 +360,13 @@ export function getAttributeArgLiteral( export function getFunctionExpressionContext(funcDecl: FunctionDecl) { const funcAllowedContext: ExpressionContext[] = []; - const funcAttr = funcDecl.attributes.find( - (attr) => attr.decl.$refText === '@@@expressionContext' - ); + const funcAttr = funcDecl.attributes.find((attr) => attr.decl.$refText === '@@@expressionContext'); if (funcAttr) { const contextArg = funcAttr.args[0]?.value; if (isArrayExpr(contextArg)) { contextArg.items.forEach((item) => { if (isEnumFieldReference(item)) { - funcAllowedContext.push( - item.target.$refText as ExpressionContext - ); + funcAllowedContext.push(item.target.$refText as ExpressionContext); } }); } @@ -452,18 +374,10 @@ export function getFunctionExpressionContext(funcDecl: FunctionDecl) { return funcAllowedContext; } -export function getFieldReference( - expr: Expression -): DataModelField | TypeDefField | undefined { - if ( - isReferenceExpr(expr) && - (isDataModelField(expr.target.ref) || isTypeDefField(expr.target.ref)) - ) { +export function getFieldReference(expr: Expression): DataModelField | TypeDefField | undefined { + if (isReferenceExpr(expr) && (isDataModelField(expr.target.ref) || isTypeDefField(expr.target.ref))) { return expr.target.ref; - } else if ( - isMemberAccessExpr(expr) && - (isDataModelField(expr.member.ref) || isTypeDefField(expr.member.ref)) - ) { + } else if (isMemberAccessExpr(expr) && (isDataModelField(expr.member.ref) || isTypeDefField(expr.member.ref))) { return expr.member.ref; } else { return undefined; @@ -471,17 +385,10 @@ export function getFieldReference( } export function isCheckInvocation(node: AstNode) { - return ( - isInvocationExpr(node) && - node.function.ref?.name === 'check' && - isFromStdlib(node.function.ref) - ); + return isInvocationExpr(node) && node.function.ref?.name === 'check' && isFromStdlib(node.function.ref); } -export function resolveTransitiveImports( - documents: LangiumDocuments, - model: Model -) { +export function resolveTransitiveImports(documents: LangiumDocuments, model: Model) { return resolveTransitiveImportsInternal(documents, model); } @@ -490,7 +397,7 @@ function resolveTransitiveImportsInternal( model: Model, initialModel = model, visited: Set = new Set(), - models: Set = new Set() + models: Set = new Set(), ) { const doc = AstUtils.getDocument(model); const initialDoc = AstUtils.getDocument(initialModel); @@ -505,13 +412,7 @@ function resolveTransitiveImportsInternal( for (const imp of model.imports) { const importedModel = resolveImport(documents, imp); if (importedModel) { - resolveTransitiveImportsInternal( - documents, - importedModel, - initialModel, - visited, - models - ); + resolveTransitiveImportsInternal(documents, importedModel, initialModel, visited, models); } } } @@ -525,10 +426,7 @@ export function resolveImport(documents: LangiumDocuments, imp: ModelImport) { let resolvedDocument = documents.getDocument(resolvedUri); if (!resolvedDocument) { const content = fs.readFileSync(resolvedUri.fsPath, 'utf-8'); - resolvedDocument = documents.createDocument( - resolvedUri, - content - ); + resolvedDocument = documents.createDocument(resolvedUri, content); } const node = resolvedDocument.parseResult.value; if (isModel(node)) { @@ -570,7 +468,7 @@ export function findNodeModulesFile(name: string, cwd: string = process.cwd()) { // Use require.resolve to find the module/file. The paths option allows specifying the directory to start from. const resolvedPath = require.resolve(name, { paths: [cwd] }); return resolvedPath; - } catch (error) { + } catch { // If require.resolve fails to find the module/file, it will throw an error. return undefined; } @@ -580,9 +478,7 @@ export function findNodeModulesFile(name: string, cwd: string = process.cwd()) { * Gets data models and type defs in the ZModel schema. */ export function getDataModelAndTypeDefs(model: Model, includeIgnored = false) { - const r = model.declarations.filter( - (d): d is DataModel | TypeDef => isDataModel(d) || isTypeDef(d) - ); + const r = model.declarations.filter((d): d is DataModel | TypeDef => isDataModel(d) || isTypeDef(d)); if (includeIgnored) { return r; } else { @@ -590,10 +486,7 @@ export function getDataModelAndTypeDefs(model: Model, includeIgnored = false) { } } -export function getAllDeclarationsIncludingImports( - documents: LangiumDocuments, - model: Model -) { +export function getAllDeclarationsIncludingImports(documents: LangiumDocuments, model: Model) { const imports = resolveTransitiveImports(documents, model); return model.declarations.concat(...imports.map((imp) => imp.declarations)); } @@ -607,42 +500,27 @@ export function getAuthDecl(decls: (DataModel | TypeDef)[]) { } export function isFutureInvocation(node: AstNode) { - return ( - isInvocationExpr(node) && - node.function.ref?.name === 'future' && - isFromStdlib(node.function.ref) - ); + return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref); } export function isCollectionPredicate(node: AstNode): node is BinaryExpr { return isBinaryExpr(node) && ['?', '!', '^'].includes(node.operator); } -export function getAllLoadedDataModelsAndTypeDefs( - langiumDocuments: LangiumDocuments -) { +export function getAllLoadedDataModelsAndTypeDefs(langiumDocuments: LangiumDocuments) { return langiumDocuments.all .map((doc) => doc.parseResult.value as Model) - .flatMap((model) => - model.declarations.filter( - (d): d is DataModel | TypeDef => isDataModel(d) || isTypeDef(d) - ) - ) + .flatMap((model) => model.declarations.filter((d): d is DataModel | TypeDef => isDataModel(d) || isTypeDef(d))) .toArray(); } -export function getAllDataModelsIncludingImports( - documents: LangiumDocuments, - model: Model -) { - return getAllDeclarationsIncludingImports(documents, model).filter( - isDataModel - ); +export function getAllDataModelsIncludingImports(documents: LangiumDocuments, model: Model) { + return getAllDeclarationsIncludingImports(documents, model).filter(isDataModel); } export function getAllLoadedAndReachableDataModelsAndTypeDefs( langiumDocuments: LangiumDocuments, - fromModel?: DataModel + fromModel?: DataModel, ) { // get all data models from loaded documents const allDataModels = getAllLoadedDataModelsAndTypeDefs(langiumDocuments); @@ -651,10 +529,7 @@ export function getAllLoadedAndReachableDataModelsAndTypeDefs( // merge data models transitively reached from the current model const model = AstUtils.getContainerOfType(fromModel, isModel); if (model) { - const transitiveDataModels = getAllDataModelsIncludingImports( - langiumDocuments, - model - ); + const transitiveDataModels = getAllDataModelsIncludingImports(langiumDocuments, model); transitiveDataModels.forEach((dm) => { if (!allDataModels.includes(dm)) { allDataModels.push(dm); @@ -666,9 +541,7 @@ export function getAllLoadedAndReachableDataModelsAndTypeDefs( return allDataModels; } -export function getContainingDataModel( - node: Expression -): DataModel | undefined { +export function getContainingDataModel(node: Expression): DataModel | undefined { let curr: AstNode | undefined = node.$container; while (curr) { if (isDataModel(curr)) { diff --git a/packages/language/src/validator.ts b/packages/language/src/validator.ts index b3c9dc7e..7b7e117c 100644 --- a/packages/language/src/validator.ts +++ b/packages/language/src/validator.ts @@ -1,9 +1,6 @@ -import type { - AstNode, - LangiumDocument, - ValidationAcceptor, - ValidationChecks, -} from 'langium'; +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +import type { AstNode, LangiumDocument, ValidationAcceptor, ValidationChecks } from 'langium'; import type { Attribute, DataModel, @@ -64,27 +61,20 @@ export class ZModelValidator { currNode = currNode.$container; } - return ( - doc?.parseResult.lexerErrors.length === 0 && - doc?.parseResult.parserErrors.length === 0 - ); + return doc?.parseResult.lexerErrors.length === 0 && doc?.parseResult.parserErrors.length === 0; } checkModel(node: Model, accept: ValidationAcceptor): void { this.shouldCheck(node) && - new SchemaValidator( - this.services.shared.workspace.LangiumDocuments - ).validate(node, accept); + new SchemaValidator(this.services.shared.workspace.LangiumDocuments).validate(node, accept); } checkDataSource(node: DataSource, accept: ValidationAcceptor): void { - this.shouldCheck(node) && - new DataSourceValidator().validate(node, accept); + this.shouldCheck(node) && new DataSourceValidator().validate(node, accept); } checkDataModel(node: DataModel, accept: ValidationAcceptor): void { - this.shouldCheck(node) && - new DataModelValidator().validate(node, accept); + this.shouldCheck(node) && new DataModelValidator().validate(node, accept); } checkTypeDef(node: TypeDef, accept: ValidationAcceptor): void { @@ -96,25 +86,18 @@ export class ZModelValidator { } checkAttribute(node: Attribute, accept: ValidationAcceptor): void { - this.shouldCheck(node) && - new AttributeValidator().validate(node, accept); + this.shouldCheck(node) && new AttributeValidator().validate(node, accept); } checkExpression(node: Expression, accept: ValidationAcceptor): void { - this.shouldCheck(node) && - new ExpressionValidator().validate(node, accept); + this.shouldCheck(node) && new ExpressionValidator().validate(node, accept); } - checkFunctionInvocation( - node: InvocationExpr, - accept: ValidationAcceptor - ): void { - this.shouldCheck(node) && - new FunctionInvocationValidator().validate(node, accept); + checkFunctionInvocation(node: InvocationExpr, accept: ValidationAcceptor): void { + this.shouldCheck(node) && new FunctionInvocationValidator().validate(node, accept); } checkFunctionDecl(node: FunctionDecl, accept: ValidationAcceptor): void { - this.shouldCheck(node) && - new FunctionDeclValidator().validate(node, accept); + this.shouldCheck(node) && new FunctionDeclValidator().validate(node, accept); } } diff --git a/packages/language/src/validators/attribute-application-validator.ts b/packages/language/src/validators/attribute-application-validator.ts index 0b3e0912..285f917f 100644 --- a/packages/language/src/validators/attribute-application-validator.ts +++ b/packages/language/src/validators/attribute-application-validator.ts @@ -37,11 +37,7 @@ const attributeCheckers = new Map(); // function handler decorator function check(name: string) { - return function ( - _target: unknown, - _propertyKey: string, - descriptor: PropertyDescriptor - ) { + return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) { if (!attributeCheckers.get(name)) { attributeCheckers.set(name, descriptor); } @@ -49,17 +45,12 @@ function check(name: string) { }; } -type AttributeApplication = - | DataModelAttribute - | DataModelFieldAttribute - | InternalAttribute; +type AttributeApplication = DataModelAttribute | DataModelFieldAttribute | InternalAttribute; /** * Validates function declarations. */ -export default class AttributeApplicationValidator - implements AstValidator -{ +export default class AttributeApplicationValidator implements AstValidator { validate(attr: AttributeApplication, accept: ValidationAcceptor) { const decl = attr.decl.ref; if (!decl) { @@ -68,42 +59,20 @@ export default class AttributeApplicationValidator const targetDecl = attr.$container; if (decl.name === '@@@targetField' && !isAttribute(targetDecl)) { - accept( - 'error', - `attribute "${decl.name}" can only be used on attribute declarations`, - { node: attr } - ); + accept('error', `attribute "${decl.name}" can only be used on attribute declarations`, { node: attr }); return; } - if ( - isDataModelField(targetDecl) && - !isValidAttributeTarget(decl, targetDecl) - ) { - accept( - 'error', - `attribute "${decl.name}" cannot be used on this type of field`, - { node: attr } - ); + if (isDataModelField(targetDecl) && !isValidAttributeTarget(decl, targetDecl)) { + accept('error', `attribute "${decl.name}" cannot be used on this type of field`, { node: attr }); } - if ( - isTypeDefField(targetDecl) && - !hasAttribute(decl, '@@@supportTypeDef') - ) { - accept( - 'error', - `attribute "${decl.name}" cannot be used on type declaration fields`, - { node: attr } - ); + if (isTypeDefField(targetDecl) && !hasAttribute(decl, '@@@supportTypeDef')) { + accept('error', `attribute "${decl.name}" cannot be used on type declaration fields`, { node: attr }); } if (isTypeDef(targetDecl) && !hasAttribute(decl, '@@@supportTypeDef')) { - accept( - 'error', - `attribute "${decl.name}" cannot be used on type declarations`, - { node: attr } - ); + accept('error', `attribute "${decl.name}" cannot be used on type declarations`, { node: attr }); } const filledParams = new Set(); @@ -111,9 +80,7 @@ export default class AttributeApplicationValidator for (const arg of attr.args) { let paramDecl: AttributeParam | undefined; if (!arg.name) { - paramDecl = decl.params.find( - (p) => p.default && !filledParams.has(p) - ); + paramDecl = decl.params.find((p) => p.default && !filledParams.has(p)); if (!paramDecl) { accept('error', `Unexpected unnamed argument`, { node: arg, @@ -123,13 +90,9 @@ export default class AttributeApplicationValidator } else { paramDecl = decl.params.find((p) => p.name === arg.name); if (!paramDecl) { - accept( - 'error', - `Attribute "${decl.name}" doesn't have a parameter named "${arg.name}"`, - { - node: arg, - } - ); + accept('error', `Attribute "${decl.name}" doesn't have a parameter named "${arg.name}"`, { + node: arg, + }); return; } } @@ -142,30 +105,21 @@ export default class AttributeApplicationValidator } if (filledParams.has(paramDecl)) { - accept( - 'error', - `Parameter "${paramDecl.name}" is already provided`, - { node: arg } - ); + accept('error', `Parameter "${paramDecl.name}" is already provided`, { node: arg }); return; } filledParams.add(paramDecl); arg.$resolvedParam = paramDecl; } - const missingParams = decl.params.filter( - (p) => !p.type.optional && !filledParams.has(p) - ); + const missingParams = decl.params.filter((p) => !p.type.optional && !filledParams.has(p)); if (missingParams.length > 0) { accept( 'error', - `Required ${pluralize( - 'parameter', - missingParams.length - )} not provided: ${missingParams + `Required ${pluralize('parameter', missingParams.length)} not provided: ${missingParams .map((p) => p.name) .join(', ')}`, - { node: attr } + { node: attr }, ); return; } @@ -180,10 +134,7 @@ export default class AttributeApplicationValidator @check('@@allow') @check('@@deny') // @ts-expect-error - private _checkModelLevelPolicy( - attr: AttributeApplication, - accept: ValidationAcceptor - ) { + private _checkModelLevelPolicy(attr: AttributeApplication, accept: ValidationAcceptor) { const kind = getStringLiteral(attr.args[0]?.value); if (!kind) { accept('error', `expects a string literal`, { @@ -191,12 +142,7 @@ export default class AttributeApplicationValidator }); return; } - this.validatePolicyKinds( - kind, - ['create', 'read', 'update', 'delete', 'all'], - attr, - accept - ); + this.validatePolicyKinds(kind, ['create', 'read', 'update', 'delete', 'all'], attr, accept); // @encrypted fields cannot be used in policy rules this.rejectEncryptedFields(attr, accept); @@ -205,10 +151,7 @@ export default class AttributeApplicationValidator @check('@allow') @check('@deny') // @ts-expect-error - private _checkFieldLevelPolicy( - attr: AttributeApplication, - accept: ValidationAcceptor - ) { + private _checkFieldLevelPolicy(attr: AttributeApplication, accept: ValidationAcceptor) { const kind = getStringLiteral(attr.args[0]?.value); if (!kind) { accept('error', `expects a string literal`, { @@ -216,23 +159,11 @@ export default class AttributeApplicationValidator }); return; } - const kindItems = this.validatePolicyKinds( - kind, - ['read', 'update', 'all'], - attr, - accept - ); + const kindItems = this.validatePolicyKinds(kind, ['read', 'update', 'all'], attr, accept); const expr = attr.args[1]?.value; - if ( - expr && - AstUtils.streamAst(expr).some((node) => isFutureExpr(node)) - ) { - accept( - 'error', - `"future()" is not allowed in field-level policy rules`, - { node: expr } - ); + if (expr && AstUtils.streamAst(expr).some((node) => isFutureExpr(node))) { + accept('error', `"future()" is not allowed in field-level policy rules`, { node: expr }); } // 'update' rules are not allowed for relation fields @@ -242,7 +173,7 @@ export default class AttributeApplicationValidator accept( 'error', `Field-level policy rules with "update" or "all" kind are not allowed for relation fields. Put rules on foreign-key fields instead.`, - { node: attr } + { node: attr }, ); } } @@ -253,33 +184,21 @@ export default class AttributeApplicationValidator @check('@@validate') // @ts-expect-error - private _checkValidate( - attr: AttributeApplication, - accept: ValidationAcceptor - ) { + private _checkValidate(attr: AttributeApplication, accept: ValidationAcceptor) { const condition = attr.args[0]?.value; if ( condition && AstUtils.streamAst(condition).some( - (node) => - isDataModelFieldReference(node) && - isDataModel(node.$resolvedType?.decl) + (node) => isDataModelFieldReference(node) && isDataModel(node.$resolvedType?.decl), ) ) { - accept( - 'error', - `\`@@validate\` condition cannot use relation fields`, - { node: condition } - ); + accept('error', `\`@@validate\` condition cannot use relation fields`, { node: condition }); } } @check('@@unique') // @ts-expect-error - private _checkUnique( - attr: AttributeApplication, - accept: ValidationAcceptor - ) { + private _checkUnique(attr: AttributeApplication, accept: ValidationAcceptor) { const fields = attr.args[0]?.value; if (!fields) { return; @@ -299,17 +218,10 @@ export default class AttributeApplicationValidator return; } - if ( - item.target.ref.$container !== attr.$container && - isDelegateModel(item.target.ref.$container) - ) { - accept( - 'error', - `Cannot use fields inherited from a polymorphic base model in \`@@unique\``, - { - node: item, - } - ); + if (item.target.ref.$container !== attr.$container && isDelegateModel(item.target.ref.$container)) { + accept('error', `Cannot use fields inherited from a polymorphic base model in \`@@unique\``, { + node: item, + }); } }); } else { @@ -319,20 +231,10 @@ export default class AttributeApplicationValidator } } - private rejectEncryptedFields( - attr: AttributeApplication, - accept: ValidationAcceptor - ) { + private rejectEncryptedFields(attr: AttributeApplication, accept: ValidationAcceptor) { AstUtils.streamAllContents(attr).forEach((node) => { - if ( - isDataModelFieldReference(node) && - hasAttribute(node.target.ref as DataModelField, '@encrypted') - ) { - accept( - 'error', - `Encrypted fields cannot be used in policy rules`, - { node } - ); + if (isDataModelFieldReference(node) && hasAttribute(node.target.ref as DataModelField, '@encrypted')) { + accept('error', `Encrypted fields cannot be used in policy rules`, { node }); } }); } @@ -341,17 +243,15 @@ export default class AttributeApplicationValidator kind: string, candidates: string[], attr: AttributeApplication, - accept: ValidationAcceptor + accept: ValidationAcceptor, ) { const items = kind.split(',').map((x) => x.trim()); items.forEach((item) => { if (!candidates.includes(item)) { accept( 'error', - `Invalid policy rule kind: "${item}", allowed: ${candidates - .map((c) => '"' + c + '"') - .join(', ')}`, - { node: attr } + `Invalid policy rule kind: "${item}", allowed: ${candidates.map((c) => '"' + c + '"').join(', ')}`, + { node: attr }, ); } }); @@ -359,11 +259,7 @@ export default class AttributeApplicationValidator } } -function assignableToAttributeParam( - arg: AttributeArg, - param: AttributeParam, - attr: AttributeApplication -): boolean { +function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, attr: AttributeApplication): boolean { const argResolvedType = arg.$resolvedType; if (!argResolvedType) { return false; @@ -398,24 +294,14 @@ function assignableToAttributeParam( // destination is field reference or transitive field reference, check if // argument is reference or array or reference - if ( - dstType === 'FieldReference' || - dstType === 'TransitiveFieldReference' - ) { + if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') { if (dstIsArray) { return ( isArrayExpr(arg.value) && - !arg.value.items.find( - (item) => - !isReferenceExpr(item) || - !isDataModelField(item.target.ref) - ) + !arg.value.items.find((item) => !isReferenceExpr(item) || !isDataModelField(item.target.ref)) ); } else { - return ( - isReferenceExpr(arg.value) && - isDataModelField(arg.value.target.ref) - ); + return isReferenceExpr(arg.value) && isDataModelField(arg.value.target.ref); } } @@ -423,20 +309,13 @@ function assignableToAttributeParam( // enum type let attrArgDeclType = dstRef?.ref; - if ( - dstType === 'ContextType' && - isDataModelField(attr.$container) && - attr.$container?.type?.reference - ) { + if (dstType === 'ContextType' && isDataModelField(attr.$container) && attr.$container?.type?.reference) { // attribute parameter type is ContextType, need to infer type from // the attribute's container attrArgDeclType = resolved(attr.$container.type.reference); dstIsArray = attr.$container.type.array; } - return ( - attrArgDeclType === argResolvedType.decl && - dstIsArray === argResolvedType.array - ); + return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array; } else if (dstType) { // scalar type @@ -452,42 +331,29 @@ function assignableToAttributeParam( if (!attr.$container?.type?.type) { return false; } - dstType = mapBuiltinTypeToExpressionType( - attr.$container.type.type - ); + dstType = mapBuiltinTypeToExpressionType(attr.$container.type.type); dstIsArray = attr.$container.type.array; } else { dstType = 'Any'; } } - return ( - typeAssignable(dstType, argResolvedType.decl, arg.value) && - dstIsArray === argResolvedType.array - ); + return typeAssignable(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array; } else { // reference type - return ( - (dstRef?.ref === argResolvedType.decl || dstType === 'Any') && - dstIsArray === argResolvedType.array - ); + return (dstRef?.ref === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array; } } -function isValidAttributeTarget( - attrDecl: Attribute, - targetDecl: DataModelField -) { - const targetField = attrDecl.attributes.find( - (attr) => attr.decl.ref?.name === '@@@targetField' - ); +function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataModelField) { + const targetField = attrDecl.attributes.find((attr) => attr.decl.ref?.name === '@@@targetField'); if (!targetField?.args[0]) { // no field type constraint return true; } const fieldTypes = (targetField.args[0].value as ArrayExpr).items.map( - (item) => (item as ReferenceExpr).target.ref?.name + (item) => (item as ReferenceExpr).target.ref?.name, ); let allowed = false; @@ -521,8 +387,7 @@ function isValidAttributeTarget( allowed = allowed || targetDecl.type.type === 'Bytes'; break; case 'ModelField': - allowed = - allowed || isDataModel(targetDecl.type.reference?.ref); + allowed = allowed || isDataModel(targetDecl.type.reference?.ref); break; case 'TypeDefField': allowed = allowed || isTypeDef(targetDecl.type.reference?.ref); @@ -538,9 +403,6 @@ function isValidAttributeTarget( return allowed; } -export function validateAttributeApplication( - attr: AttributeApplication, - accept: ValidationAcceptor -) { +export function validateAttributeApplication(attr: AttributeApplication, accept: ValidationAcceptor) { new AttributeApplicationValidator().validate(attr, accept); } diff --git a/packages/language/src/validators/attribute-validator.ts b/packages/language/src/validators/attribute-validator.ts index 9191ffc6..be600432 100644 --- a/packages/language/src/validators/attribute-validator.ts +++ b/packages/language/src/validators/attribute-validator.ts @@ -8,8 +8,6 @@ import type { AstValidator } from './common'; */ export default class AttributeValidator implements AstValidator { validate(attr: Attribute, accept: ValidationAcceptor): void { - attr.attributes.forEach((attr) => - validateAttributeApplication(attr, accept) - ); + attr.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); } } diff --git a/packages/language/src/validators/common.ts b/packages/language/src/validators/common.ts index eecf368c..76bd41cc 100644 --- a/packages/language/src/validators/common.ts +++ b/packages/language/src/validators/common.ts @@ -17,11 +17,9 @@ export interface AstValidator { export function validateDuplicatedDeclarations( container: AstNode, decls: Array, - accept: ValidationAcceptor + accept: ValidationAcceptor, ): void { - const groupByName = decls.reduce< - Record> - >((group, decl) => { + const groupByName = decls.reduce>>((group, decl) => { group[decl.name] = group[decl.name] ?? []; group[decl.name]!.push(decl); return group; @@ -31,9 +29,7 @@ export function validateDuplicatedDeclarations( if (decls.length > 1) { let errorField = decls[1]!; if (isDataModelField(decls[0])) { - const nonInheritedFields = decls.filter( - (x) => !(isDataModelField(x) && x.$container !== container) - ); + const nonInheritedFields = decls.filter((x) => !(isDataModelField(x) && x.$container !== container)); if (nonInheritedFields.length > 0) { errorField = nonInheritedFields.slice(-1)[0]!; } diff --git a/packages/language/src/validators/datamodel-validator.ts b/packages/language/src/validators/datamodel-validator.ts index 2c44c729..877f92e8 100644 --- a/packages/language/src/validators/datamodel-validator.ts +++ b/packages/language/src/validators/datamodel-validator.ts @@ -1,9 +1,4 @@ -import { - AstUtils, - type AstNode, - type DiagnosticInfo, - type ValidationAcceptor, -} from 'langium'; +import { AstUtils, type AstNode, type DiagnosticInfo, type ValidationAcceptor } from 'langium'; import { IssueCodes, SCALAR_TYPES } from '../constants'; import { ArrayExpr, @@ -49,12 +44,8 @@ export default class DataModelValidator implements AstValidator { private validateFields(dm: DataModel, accept: ValidationAcceptor) { const allFields = getModelFieldsWithBases(dm); - const idFields = allFields.filter((f) => - f.attributes.find((attr) => attr.decl.ref?.name === '@id') - ); - const uniqueFields = allFields.filter((f) => - f.attributes.find((attr) => attr.decl.ref?.name === '@unique') - ); + const idFields = allFields.filter((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@id')); + const uniqueFields = allFields.filter((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@unique')); const modelLevelIds = getModelIdFields(dm); const modelUniqueFields = getModelUniqueFields(dm); @@ -70,49 +61,29 @@ export default class DataModelValidator implements AstValidator { '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.', { node: dm, - } + }, ); } else if (idFields.length > 0 && modelLevelIds.length > 0) { - accept( - 'error', - 'Model cannot have both field-level @id and model-level @@id attributes', - { - node: dm, - } - ); + accept('error', 'Model cannot have both field-level @id and model-level @@id attributes', { + node: dm, + }); } else if (idFields.length > 1) { - accept( - 'error', - 'Model can include at most one field with @id attribute', - { - node: dm, - } - ); + accept('error', 'Model can include at most one field with @id attribute', { + node: dm, + }); } else { - const fieldsToCheck = - idFields.length > 0 ? idFields : modelLevelIds; + const fieldsToCheck = idFields.length > 0 ? idFields : modelLevelIds; fieldsToCheck.forEach((idField) => { if (idField.type.optional) { - accept( - 'error', - 'Field with @id attribute must not be optional', - { node: idField } - ); + accept('error', 'Field with @id attribute must not be optional', { 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); + 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 } - ); + accept('error', 'Field with @id attribute must be of scalar or enum type', { node: idField }); } }); } @@ -128,53 +99,27 @@ export default class DataModelValidator implements AstValidator { } } - private validateField( - field: DataModelField, - accept: ValidationAcceptor - ): void { + private validateField(field: DataModelField, accept: ValidationAcceptor): void { if (field.type.array && field.type.optional) { - accept( - 'error', - 'Optional lists are not supported. Use either `Type[]` or `Type?`', - { node: field.type } - ); + accept('error', 'Optional lists are not supported. Use either `Type[]` or `Type?`', { node: field.type }); } - if ( - field.type.unsupported && - !isStringLiteral(field.type.unsupported.value) - ) { - accept( - 'error', - 'Unsupported type argument must be a string literal', - { node: field.type.unsupported } - ); + if (field.type.unsupported && !isStringLiteral(field.type.unsupported.value)) { + accept('error', 'Unsupported type argument must be a string literal', { node: field.type.unsupported }); } if (field.type.array && !isDataModel(field.type.reference?.ref)) { - const provider = this.getDataSourceProvider( - AstUtils.getContainerOfType(field, isModel)! - ); + const provider = this.getDataSourceProvider(AstUtils.getContainerOfType(field, isModel)!); if (provider === 'sqlite') { - accept( - 'error', - `Array type is not supported for "${provider}" provider.`, - { node: field.type } - ); + accept('error', `Array type is not supported for "${provider}" provider.`, { node: field.type }); } } - field.attributes.forEach((attr) => - validateAttributeApplication(attr, accept) - ); + field.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); if (isTypeDef(field.type.reference?.ref)) { if (!hasAttribute(field, '@json')) { - accept( - 'error', - 'Custom-typed field must have @json attribute', - { node: field } - ); + accept('error', 'Custom-typed field must have @json attribute', { node: field }); } } } @@ -192,15 +137,11 @@ export default class DataModelValidator implements AstValidator { } private validateAttributes(dm: DataModel, accept: ValidationAcceptor) { - dm.attributes.forEach((attr) => - validateAttributeApplication(attr, accept) - ); + dm.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); } private parseRelation(field: DataModelField, accept?: ValidationAcceptor) { - const relAttr = field.attributes.find( - (attr) => attr.decl.ref?.name === '@relation' - ); + const relAttr = field.attributes.find((attr) => attr.decl.ref?.name === '@relation'); let name: string | undefined; let fields: ReferenceExpr[] | undefined; @@ -245,21 +186,13 @@ export default class DataModelValidator implements AstValidator { if (!fields || !references) { if (accept) { - accept( - 'error', - `"fields" and "references" must be provided together`, - { node: relAttr } - ); + accept('error', `"fields" and "references" must be provided together`, { node: relAttr }); } } else { // validate "fields" and "references" typing consistency if (fields.length !== references.length) { if (accept) { - accept( - 'error', - `"references" and "fields" must have the same length`, - { node: relAttr } - ); + accept('error', `"references" and "fields" must have the same length`, { node: relAttr }); } } else { for (let i = 0; i < fields.length; i++) { @@ -268,16 +201,13 @@ export default class DataModelValidator implements AstValidator { continue; } - if ( - !field.type.optional && - fieldRef.$resolvedType?.nullable - ) { + if (!field.type.optional && fieldRef.$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 "${fieldRef.target.$refText}" is optional`, - { node: fieldRef.target.ref! } + { node: fieldRef.target.ref! }, ); } } @@ -298,19 +228,13 @@ export default class DataModelValidator implements AstValidator { } if ( - fieldRef.$resolvedType?.decl !== - references[i]?.$resolvedType?.decl || - fieldRef.$resolvedType?.array !== - references[i]?.$resolvedType?.array + fieldRef.$resolvedType?.decl !== references[i]?.$resolvedType?.decl || + fieldRef.$resolvedType?.array !== references[i]?.$resolvedType?.array ) { if (accept) { - accept( - 'error', - `values of "references" and "fields" must have the same type`, - { - node: relAttr, - } - ); + accept('error', `values of "references" and "fields" must have the same type`, { + node: relAttr, + }); } } } @@ -324,11 +248,7 @@ export default class DataModelValidator implements AstValidator { return field.type.reference?.ref === field.$container; } - private validateRelationField( - contextModel: DataModel, - field: DataModelField, - accept: ValidationAcceptor - ) { + private validateRelationField(contextModel: DataModel, field: DataModelField, accept: ValidationAcceptor) { const thisRelation = this.parseRelation(field, accept); if (!thisRelation.valid) { return; @@ -339,14 +259,13 @@ export default class DataModelValidator implements AstValidator { return; } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const oppositeModel = field.type.reference!.ref! as DataModel; // Use name because the current document might be updated - let oppositeFields = getModelFieldsWithBases( - oppositeModel, - false - ).filter((f) => f.type.reference?.ref?.name === contextModel.name); + let oppositeFields = getModelFieldsWithBases(oppositeModel, false).filter( + (f) => f.type.reference?.ref?.name === contextModel.name, + ); oppositeFields = oppositeFields.filter((f) => { const fieldRel = this.parseRelation(f); return fieldRel.valid && fieldRel.name === thisRelation.name; @@ -361,8 +280,7 @@ export default class DataModelValidator implements AstValidator { info.property = 'name'; const container = field.$container; - const relationFieldDocUri = - AstUtils.getDocument(container).textDocument.uri; + const relationFieldDocUri = AstUtils.getDocument(container).textDocument.uri; const relationDataModelName = container.name; const data: MissingOppositeRelationData = { @@ -377,7 +295,7 @@ export default class DataModelValidator implements AstValidator { accept( 'error', `The relation field "${field.name}" on model "${contextModel.name}" is missing an opposite relation field on model "${oppositeModel.name}"`, - info + info, ); return; } else if (oppositeFields.length > 1) { @@ -390,14 +308,10 @@ export default class DataModelValidator implements AstValidator { } else { accept( 'error', - `Fields ${oppositeFields - .map((f) => '"' + f.name + '"') - .join(', ')} on model "${ + `Fields ${oppositeFields.map((f) => '"' + f.name + '"').join(', ')} on model "${ oppositeModel.name - }" refer to the same relation to model "${ - field.$container.name - }"`, - { node: f } + }" refer to the same relation to model "${field.$container.name}"`, + { node: f }, ); } }); @@ -411,29 +325,18 @@ export default class DataModelValidator implements AstValidator { if (thisRelation?.references?.length && thisRelation.fields?.length) { if (oppositeRelation?.references || oppositeRelation?.fields) { - accept( - 'error', - '"fields" and "references" must be provided only on one side of relation field', - { - node: oppositeField, - } - ); + accept('error', '"fields" and "references" must be provided only on one side of relation field', { + node: oppositeField, + }); return; } else { relationOwner = oppositeField; } - } else if ( - oppositeRelation?.references?.length && - oppositeRelation.fields?.length - ) { + } else if (oppositeRelation?.references?.length && oppositeRelation.fields?.length) { if (thisRelation?.references || thisRelation?.fields) { - accept( - 'error', - '"fields" and "references" must be provided only on one side of relation field', - { - node: field, - } - ); + accept('error', '"fields" and "references" must be provided only on one side of relation field', { + node: field, + }); return; } else { relationOwner = field; @@ -446,7 +349,7 @@ export default class DataModelValidator implements AstValidator { accept( 'error', 'Field for one side of relation must carry @relation attribute with both "fields" and "references"', - { node: f } + { node: f }, ); } }); @@ -487,24 +390,16 @@ export default class DataModelValidator implements AstValidator { thisRelation.fields?.forEach((ref) => { const refField = ref.target.ref as DataModelField; if (refField) { - if ( - refField.attributes.find( - (a) => - a.decl.ref?.name === '@id' || - a.decl.ref?.name === '@unique' - ) - ) { + if (refField.attributes.find((a) => a.decl.ref?.name === '@id' || a.decl.ref?.name === '@unique')) { return; } - if ( - uniqueFieldList.some((list) => list.includes(refField)) - ) { + if (uniqueFieldList.some((list) => list.includes(refField))) { return; } accept( 'error', `Field "${refField.name}" on model "${containingModel.name}" is part of a one-to-one relation and must be marked as @unique or be part of a model-level @@unique attribute`, - { node: refField } + { node: refField }, ); } }); @@ -512,14 +407,8 @@ export default class DataModelValidator implements AstValidator { } // checks if the given field is inherited directly or indirectly from a delegate model - private isFieldInheritedFromDelegateModel( - field: DataModelField, - contextModel: DataModel - ) { - const basePath = findUpInheritance( - contextModel, - field.$container as DataModel - ); + private isFieldInheritedFromDelegateModel(field: DataModelField, contextModel: DataModel) { + const basePath = findUpInheritance(contextModel, field.$container as DataModel); if (basePath && basePath.some(isDelegateModel)) { return true; } else { @@ -527,16 +416,11 @@ export default class DataModelValidator implements AstValidator { } } - private validateBaseAbstractModel( - model: DataModel, - accept: ValidationAcceptor - ) { + private validateBaseAbstractModel(model: DataModel, accept: ValidationAcceptor) { model.superTypes.forEach((superType, index) => { if ( !superType.ref?.isAbstract && - !superType.ref?.attributes.some( - (attr) => attr.decl.ref?.name === '@@delegate' - ) + !superType.ref?.attributes.some((attr) => attr.decl.ref?.name === '@@delegate') ) accept( 'error', @@ -545,47 +429,32 @@ export default class DataModelValidator implements AstValidator { node: model, property: 'superTypes', index, - } + }, ); }); } - private validateBaseDelegateModel( - model: DataModel, - accept: ValidationAcceptor - ) { - if ( - model.superTypes.filter( - (base) => base.ref && isDelegateModel(base.ref) - ).length > 1 - ) { - accept( - 'error', - 'Extending from multiple delegate models is not supported', - { - node: model, - property: 'superTypes', - } - ); + private validateBaseDelegateModel(model: DataModel, accept: ValidationAcceptor) { + if (model.superTypes.filter((base) => base.ref && isDelegateModel(base.ref)).length > 1) { + accept('error', 'Extending from multiple delegate models is not supported', { + node: model, + property: 'superTypes', + }); } } private validateInheritance(dm: DataModel, accept: ValidationAcceptor) { const seen = [dm]; - const todo: DataModel[] = dm.superTypes.map( - (superType) => superType.ref! - ); + const todo: DataModel[] = dm.superTypes.map((superType) => superType.ref!); while (todo.length > 0) { const current = todo.shift()!; if (seen.includes(current)) { accept( 'error', - `Circular inheritance detected: ${seen - .map((m) => m.name) - .join(' -> ')} -> ${current.name}`, + `Circular inheritance detected: ${seen.map((m) => m.name).join(' -> ')} -> ${current.name}`, { node: dm, - } + }, ); return; } diff --git a/packages/language/src/validators/datasource-validator.ts b/packages/language/src/validators/datasource-validator.ts index 8e1c7350..c2540426 100644 --- a/packages/language/src/validators/datasource-validator.ts +++ b/packages/language/src/validators/datasource-validator.ts @@ -32,10 +32,10 @@ export default class DataSourceValidator implements AstValidator { } else if (!SUPPORTED_PROVIDERS.includes(value)) { accept( 'error', - `Provider "${value}" is not supported. Choose from ${SUPPORTED_PROVIDERS.map( - (p) => '"' + p + '"' - ).join(' | ')}.`, - { node: provider.value } + `Provider "${value}" is not supported. Choose from ${SUPPORTED_PROVIDERS.map((p) => '"' + p + '"').join( + ' | ', + )}.`, + { node: provider.value }, ); } } @@ -54,20 +54,10 @@ export default class DataSourceValidator implements AstValidator { continue; } const value = getStringLiteral(field.value); - if ( - !value && - !( - isInvocationExpr(field.value) && - field.value.function.ref?.name === 'env' - ) - ) { - accept( - 'error', - `"${fieldName}" must be set to a string literal or an invocation of "env" function`, - { - node: field.value, - } - ); + if (!value && !(isInvocationExpr(field.value) && field.value.function.ref?.name === 'env')) { + accept('error', `"${fieldName}" must be set to a string literal or an invocation of "env" function`, { + node: field.value, + }); } } } @@ -77,11 +67,7 @@ export default class DataSourceValidator implements AstValidator { if (field) { const val = getStringLiteral(field.value); if (!val || !['foreignKeys', 'prisma'].includes(val)) { - accept( - 'error', - '"relationMode" must be set to "foreignKeys" or "prisma"', - { node: field.value } - ); + accept('error', '"relationMode" must be set to "foreignKeys" or "prisma"', { node: field.value }); } } } diff --git a/packages/language/src/validators/enum-validator.ts b/packages/language/src/validators/enum-validator.ts index a434bc97..bc1574d8 100644 --- a/packages/language/src/validators/enum-validator.ts +++ b/packages/language/src/validators/enum-validator.ts @@ -7,7 +7,6 @@ import { validateDuplicatedDeclarations, type AstValidator } from './common'; * Validates enum declarations. */ export default class EnumValidator implements AstValidator { - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types validate(_enum: Enum, accept: ValidationAcceptor) { validateDuplicatedDeclarations(_enum, _enum.fields, accept); this.validateAttributes(_enum, accept); @@ -17,14 +16,10 @@ export default class EnumValidator implements AstValidator { } private validateAttributes(_enum: Enum, accept: ValidationAcceptor) { - _enum.attributes.forEach((attr) => - validateAttributeApplication(attr, accept) - ); + _enum.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); } private validateField(field: EnumField, accept: ValidationAcceptor) { - field.attributes.forEach((attr) => - validateAttributeApplication(attr, accept) - ); + field.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); } } diff --git a/packages/language/src/validators/expression-validator.ts b/packages/language/src/validators/expression-validator.ts index 39ce761a..362aed2d 100644 --- a/packages/language/src/validators/expression-validator.ts +++ b/packages/language/src/validators/expression-validator.ts @@ -36,12 +36,10 @@ export default class ExpressionValidator implements AstValidator { accept( 'error', 'auth() cannot be resolved because no model marked with "@@auth()" or named "User" is found', - { node: expr } + { node: expr }, ); } else { - const hasReferenceResolutionError = AstUtils.streamAst( - expr - ).some((node) => { + const hasReferenceResolutionError = AstUtils.streamAst(expr).some((node) => { if (isMemberAccessExpr(node)) { return !!node.member.error; } @@ -70,15 +68,8 @@ export default class ExpressionValidator implements AstValidator { private validateBinaryExpr(expr: BinaryExpr, accept: ValidationAcceptor) { switch (expr.operator) { case 'in': { - if ( - typeof expr.left.$resolvedType?.decl !== 'string' && - !isEnum(expr.left.$resolvedType?.decl) - ) { - accept( - 'error', - 'left operand of "in" must be of scalar type', - { node: expr.left } - ); + if (typeof expr.left.$resolvedType?.decl !== 'string' && !isEnum(expr.left.$resolvedType?.decl)) { + accept('error', 'left operand of "in" must be of scalar type', { node: expr.left }); } if (!expr.right.$resolvedType?.array) { @@ -121,34 +112,23 @@ export default class ExpressionValidator implements AstValidator { typeof expr.left.$resolvedType?.decl !== 'string' || !supportedShapes.includes(expr.left.$resolvedType.decl) ) { - accept( - 'error', - `invalid operand type for "${expr.operator}" operator`, - { - node: expr.left, - } - ); + accept('error', `invalid operand type for "${expr.operator}" operator`, { + node: expr.left, + }); return; } if ( typeof expr.right.$resolvedType?.decl !== 'string' || !supportedShapes.includes(expr.right.$resolvedType.decl) ) { - accept( - 'error', - `invalid operand type for "${expr.operator}" operator`, - { - node: expr.right, - } - ); + accept('error', `invalid operand type for "${expr.operator}" operator`, { + node: expr.right, + }); return; } // DateTime comparison is only allowed between two DateTime values - if ( - expr.left.$resolvedType.decl === 'DateTime' && - expr.right.$resolvedType.decl !== 'DateTime' - ) { + if (expr.left.$resolvedType.decl === 'DateTime' && expr.right.$resolvedType.decl !== 'DateTime') { accept('error', 'incompatible operand types', { node: expr, }); @@ -169,19 +149,14 @@ export default class ExpressionValidator implements AstValidator { // in validation context, all fields are optional, so we should allow // comparing any field against null if ( - (isDataModelFieldReference(expr.left) && - isNullExpr(expr.right)) || - (isDataModelFieldReference(expr.right) && - isNullExpr(expr.left)) + (isDataModelFieldReference(expr.left) && isNullExpr(expr.right)) || + (isDataModelFieldReference(expr.right) && isNullExpr(expr.left)) ) { return; } } - if ( - !!expr.left.$resolvedType?.array !== - !!expr.right.$resolvedType?.array - ) { + if (!!expr.left.$resolvedType?.array !== !!expr.right.$resolvedType?.array) { accept('error', 'incompatible operand types', { node: expr, }); @@ -189,10 +164,8 @@ export default class ExpressionValidator implements AstValidator { } if ( - (expr.left.$resolvedType?.nullable && - isNullExpr(expr.right)) || - (expr.right.$resolvedType?.nullable && - isNullExpr(expr.left)) + (expr.left.$resolvedType?.nullable && isNullExpr(expr.right)) || + (expr.right.$resolvedType?.nullable && isNullExpr(expr.left)) ) { // comparing nullable field with null return; @@ -204,14 +177,8 @@ export default class ExpressionValidator implements AstValidator { ) { // scalar types assignability if ( - !typeAssignable( - expr.left.$resolvedType.decl, - expr.right.$resolvedType.decl - ) && - !typeAssignable( - expr.right.$resolvedType.decl, - expr.left.$resolvedType.decl - ) + !typeAssignable(expr.left.$resolvedType.decl, expr.right.$resolvedType.decl) && + !typeAssignable(expr.right.$resolvedType.decl, expr.left.$resolvedType.decl) ) { accept('error', 'incompatible operand types', { node: expr, @@ -238,24 +205,14 @@ export default class ExpressionValidator implements AstValidator { // - foo == this if ( isDataModelFieldReference(expr.left) && - (isThisExpr(expr.right) || - isDataModelFieldReference(expr.right)) + (isThisExpr(expr.right) || isDataModelFieldReference(expr.right)) ) { - accept( - 'error', - 'comparison between model-typed fields are not supported', - { node: expr } - ); + accept('error', 'comparison between model-typed fields are not supported', { node: expr }); } else if ( isDataModelFieldReference(expr.right) && - (isThisExpr(expr.left) || - isDataModelFieldReference(expr.left)) + (isThisExpr(expr.left) || isDataModelFieldReference(expr.left)) ) { - accept( - 'error', - 'comparison between model-typed fields are not supported', - { node: expr } - ); + accept('error', 'comparison between model-typed fields are not supported', { node: expr }); } } else if ( (isDataModel(leftType) && !isNullExpr(expr.right)) || @@ -277,25 +234,15 @@ export default class ExpressionValidator implements AstValidator { } } - private validateCollectionPredicate( - expr: BinaryExpr, - accept: ValidationAcceptor - ) { + private validateCollectionPredicate(expr: BinaryExpr, accept: ValidationAcceptor) { if (!expr.$resolvedType) { - accept( - 'error', - 'collection predicate can only be used on an array of model type', - { node: expr } - ); + accept('error', 'collection predicate can only be used on an array of model type', { node: expr }); return; } } private isInValidationContext(node: AstNode) { - return findUpAst( - node, - (n) => isDataModelAttribute(n) && n.decl.$refText === '@@validate' - ); + return findUpAst(node, (n) => isDataModelAttribute(n) && n.decl.$refText === '@@validate'); } private isNotModelFieldExpr(expr: Expression): boolean { @@ -309,8 +256,7 @@ export default class ExpressionValidator implements AstValidator { // `auth()` access isAuthOrAuthMemberAccess(expr) || // array - (isArrayExpr(expr) && - expr.items.every((item) => this.isNotModelFieldExpr(item))) + (isArrayExpr(expr) && expr.items.every((item) => this.isNotModelFieldExpr(item))) ); } } diff --git a/packages/language/src/validators/function-decl-validator.ts b/packages/language/src/validators/function-decl-validator.ts index bf789705..31e338c1 100644 --- a/packages/language/src/validators/function-decl-validator.ts +++ b/packages/language/src/validators/function-decl-validator.ts @@ -6,12 +6,8 @@ import type { AstValidator } from './common'; /** * Validates function declarations. */ -export default class FunctionDeclValidator - implements AstValidator -{ +export default class FunctionDeclValidator implements AstValidator { validate(funcDecl: FunctionDecl, accept: ValidationAcceptor) { - funcDecl.attributes.forEach((attr) => - validateAttributeApplication(attr, accept) - ); + funcDecl.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); } } diff --git a/packages/language/src/validators/function-invocation-validator.ts b/packages/language/src/validators/function-invocation-validator.ts index f9f1e4de..ae59f5fd 100644 --- a/packages/language/src/validators/function-invocation-validator.ts +++ b/packages/language/src/validators/function-invocation-validator.ts @@ -29,11 +29,7 @@ const invocationCheckers = new Map(); // function handler decorator function func(name: string) { - return function ( - _target: unknown, - _propertyKey: string, - descriptor: PropertyDescriptor - ) { + return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) { if (!invocationCheckers.get(name)) { invocationCheckers.set(name, descriptor); } @@ -43,9 +39,7 @@ function func(name: string) { /** * InvocationExpr validation */ -export default class FunctionInvocationValidator - implements AstValidator -{ +export default class FunctionInvocationValidator implements AstValidator { validate(expr: InvocationExpr, accept: ValidationAcceptor): void { const funcDecl = expr.function.ref; if (!funcDecl) { @@ -62,15 +56,9 @@ export default class FunctionInvocationValidator // find the containing attribute context for the invocation let curr: AstNode | undefined = expr.$container; - let containerAttribute: - | DataModelAttribute - | DataModelFieldAttribute - | undefined; + let containerAttribute: DataModelAttribute | DataModelFieldAttribute | undefined; while (curr) { - if ( - isDataModelAttribute(curr) || - isDataModelFieldAttribute(curr) - ) { + if (isDataModelAttribute(curr) || isDataModelFieldAttribute(curr)) { containerAttribute = curr; break; } @@ -80,10 +68,7 @@ export default class FunctionInvocationValidator // validate the context allowed for the function const exprContext = match(containerAttribute?.decl.$refText) .with('@default', () => ExpressionContext.DefaultValue) - .with( - P.union('@@allow', '@@deny', '@allow', '@deny'), - () => ExpressionContext.AccessPolicy - ) + .with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy) .with('@@validate', () => ExpressionContext.ValidationRule) .with('@@index', () => ExpressionContext.Index) .otherwise(() => undefined); @@ -92,37 +77,21 @@ export default class FunctionInvocationValidator const funcAllowedContext = getFunctionExpressionContext(funcDecl); if (exprContext && !funcAllowedContext.includes(exprContext)) { - accept( - 'error', - `function "${funcDecl.name}" is not allowed in the current context: ${exprContext}`, - { - node: expr, - } - ); + accept('error', `function "${funcDecl.name}" is not allowed in the current context: ${exprContext}`, { + node: expr, + }); return; } // TODO: express function validation rules declaratively in ZModel - const allCasing = [ - 'original', - 'upper', - 'lower', - 'capitalize', - 'uncapitalize', - ]; + const allCasing = ['original', 'upper', 'lower', 'capitalize', 'uncapitalize']; if (['currentModel', 'currentOperation'].includes(funcDecl.name)) { const arg = getLiteral(expr.args[0]?.value); if (arg && !allCasing.includes(arg)) { - accept( - 'error', - `argument must be one of: ${allCasing - .map((c) => '"' + c + '"') - .join(', ')}`, - { - node: expr.args[0]!, - } - ); + accept('error', `argument must be one of: ${allCasing.map((c) => '"' + c + '"').join(', ')}`, { + node: expr.args[0]!, + }); } } } @@ -134,11 +103,7 @@ export default class FunctionInvocationValidator } } - private validateArgs( - funcDecl: FunctionDecl, - args: Argument[], - accept: ValidationAcceptor - ) { + private validateArgs(funcDecl: FunctionDecl, args: Argument[], accept: ValidationAcceptor) { let success = true; for (let i = 0; i < funcDecl.params.length; i++) { const param = funcDecl.params[i]; @@ -148,11 +113,7 @@ export default class FunctionInvocationValidator const arg = args[i]; if (!arg) { if (!param.optional) { - accept( - 'error', - `missing argument for parameter "${param.name}"`, - { node: funcDecl } - ); + accept('error', `missing argument for parameter "${param.name}"`, { node: funcDecl }); success = false; } } else { @@ -165,11 +126,7 @@ export default class FunctionInvocationValidator return success; } - private validateInvocationArg( - arg: Argument, - param: FunctionParam, - accept: ValidationAcceptor - ) { + private validateInvocationArg(arg: Argument, param: FunctionParam, accept: ValidationAcceptor) { const argResolvedType = arg?.value?.$resolvedType; if (!argResolvedType) { accept('error', 'argument type cannot be resolved', { node: arg }); @@ -194,10 +151,7 @@ export default class FunctionInvocationValidator if (typeof argResolvedType.decl === 'string') { // scalar type - if ( - !typeAssignable(dstType, argResolvedType.decl, arg.value) || - dstIsArray !== argResolvedType.array - ) { + if (!typeAssignable(dstType, argResolvedType.decl, arg.value) || dstIsArray !== argResolvedType.array) { accept('error', `argument is not assignable to parameter`, { node: arg, }); @@ -205,10 +159,7 @@ export default class FunctionInvocationValidator } } else { // enum or model type - if ( - (dstRef?.ref !== argResolvedType.decl && dstType !== 'Any') || - dstIsArray !== argResolvedType.array - ) { + if ((dstRef?.ref !== argResolvedType.decl && dstType !== 'Any') || dstIsArray !== argResolvedType.array) { accept('error', `argument is not assignable to parameter`, { node: arg, }); @@ -225,10 +176,7 @@ export default class FunctionInvocationValidator let valid = true; const fieldArg = expr.args[0]!.value; - if ( - !isDataModelFieldReference(fieldArg) || - !isDataModel(fieldArg.$resolvedType?.decl) - ) { + if (!isDataModelFieldReference(fieldArg) || !isDataModel(fieldArg.$resolvedType?.decl)) { accept('error', 'argument must be a relation field', { node: expr.args[0]!, }); @@ -245,15 +193,8 @@ export default class FunctionInvocationValidator const opArg = expr.args[1]?.value; if (opArg) { const operation = getLiteral(opArg); - if ( - !operation || - !['read', 'create', 'update', 'delete'].includes(operation) - ) { - accept( - 'error', - 'argument must be a "read", "create", "update", or "delete"', - { node: expr.args[1]! } - ); + if (!operation || !['read', 'create', 'update', 'delete'].includes(operation)) { + accept('error', 'argument must be a "read", "create", "update", or "delete"', { node: expr.args[1]! }); valid = false; } } @@ -279,11 +220,7 @@ export default class FunctionInvocationValidator if (seen.has(currModel)) { if (currModel === start) { - accept( - 'error', - 'cyclic dependency detected when following the `check()` call', - { node: expr } - ); + accept('error', 'cyclic dependency detected when following the `check()` call', { node: expr }); } else { // a cycle is detected but it doesn't start from the invocation expression we're checking, // just break here and the cycle will be reported when we validate the start of it @@ -294,9 +231,7 @@ export default class FunctionInvocationValidator } const policyAttrs = currModel.attributes.filter( - (attr) => - attr.decl.$refText === '@@allow' || - attr.decl.$refText === '@@deny' + (attr) => attr.decl.$refText === '@@allow' || attr.decl.$refText === '@@deny', ); for (const attr of policyAttrs) { const rule = attr.args[1]; diff --git a/packages/language/src/validators/schema-validator.ts b/packages/language/src/validators/schema-validator.ts index 57cfa4f3..5d856380 100644 --- a/packages/language/src/validators/schema-validator.ts +++ b/packages/language/src/validators/schema-validator.ts @@ -1,11 +1,7 @@ import type { LangiumDocuments, ValidationAcceptor } from 'langium'; import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../constants'; import { isDataSource, type Model } from '../generated/ast'; -import { - getAllDeclarationsIncludingImports, - resolveImport, - resolveTransitiveImports, -} from '../utils'; +import { getAllDeclarationsIncludingImports, resolveImport, resolveTransitiveImports } from '../utils'; import { validateDuplicatedDeclarations, type AstValidator } from './common'; /** @@ -14,29 +10,20 @@ import { validateDuplicatedDeclarations, type AstValidator } from './common'; export default class SchemaValidator implements AstValidator { constructor(protected readonly documents: LangiumDocuments) {} - async validate(model: Model, accept: ValidationAcceptor) { - await this.validateImports(model, accept); + validate(model: Model, accept: ValidationAcceptor) { + this.validateImports(model, accept); validateDuplicatedDeclarations(model, model.declarations, accept); - const importedModels = await resolveTransitiveImports( - this.documents, - model - ); + const importedModels = resolveTransitiveImports(this.documents, model); - const importedNames = new Set( - importedModels.flatMap((m) => m.declarations.map((d) => d.name)) - ); + const importedNames = new Set(importedModels.flatMap((m) => m.declarations.map((d) => d.name))); for (const declaration of model.declarations) { if (importedNames.has(declaration.name)) { - accept( - 'error', - `A ${declaration.name} already exists in an imported module`, - { - node: declaration, - property: 'name', - } - ); + accept('error', `A ${declaration.name} already exists in an imported module`, { + node: declaration, + property: 'name', + }); } } @@ -48,35 +35,24 @@ export default class SchemaValidator implements AstValidator { } } - private async validateDataSources( - model: Model, - accept: ValidationAcceptor - ) { - const dataSources = ( - await getAllDeclarationsIncludingImports(this.documents, model) - ).filter((d) => isDataSource(d)); + private async validateDataSources(model: Model, accept: ValidationAcceptor) { + const dataSources = (await getAllDeclarationsIncludingImports(this.documents, model)).filter((d) => + isDataSource(d), + ); if (dataSources.length > 1) { - accept( - 'error', - 'Multiple datasource declarations are not allowed', - { node: dataSources[1]! } - ); + accept('error', 'Multiple datasource declarations are not allowed', { node: dataSources[1]! }); } } - private async validateImports(model: Model, accept: ValidationAcceptor) { - await Promise.all( - model.imports.map(async (imp) => { - const importedModel = await resolveImport(this.documents, imp); - const importPath = imp.path.endsWith('.zmodel') - ? imp.path - : `${imp.path}.zmodel`; - if (!importedModel) { - accept('error', `Cannot find model file ${importPath}`, { - node: imp, - }); - } - }) - ); + 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 ${importPath}`, { + node: imp, + }); + } + }); } } diff --git a/packages/language/src/validators/typedef-validator.ts b/packages/language/src/validators/typedef-validator.ts index ecd6f51c..259608e5 100644 --- a/packages/language/src/validators/typedef-validator.ts +++ b/packages/language/src/validators/typedef-validator.ts @@ -14,21 +14,14 @@ export default class TypeDefValidator implements AstValidator { } private validateAttributes(typeDef: TypeDef, accept: ValidationAcceptor) { - typeDef.attributes.forEach((attr) => - validateAttributeApplication(attr, accept) - ); + typeDef.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); } private validateFields(typeDef: TypeDef, accept: ValidationAcceptor) { typeDef.fields.forEach((field) => this.validateField(field, accept)); } - private validateField( - field: TypeDefField, - accept: ValidationAcceptor - ): void { - field.attributes.forEach((attr) => - validateAttributeApplication(attr, accept) - ); + private validateField(field: TypeDefField, accept: ValidationAcceptor): void { + field.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); } } diff --git a/packages/language/src/zmodel-linker.ts b/packages/language/src/zmodel-linker.ts index d6d57944..40c2019f 100644 --- a/packages/language/src/zmodel-linker.ts +++ b/packages/language/src/zmodel-linker.ts @@ -84,20 +84,12 @@ export class ZModelLinker extends DefaultLinker { //#region Reference linking - override async link( - document: LangiumDocument, - cancelToken = Cancellation.CancellationToken.None - ): Promise { - if ( - document.parseResult.lexerErrors?.length > 0 || - document.parseResult.parserErrors?.length > 0 - ) { + override async link(document: LangiumDocument, cancelToken = Cancellation.CancellationToken.None): Promise { + if (document.parseResult.lexerErrors?.length > 0 || document.parseResult.parserErrors?.length > 0) { return; } - for (const node of AstUtils.streamContents( - document.parseResult.value - )) { + for (const node of AstUtils.streamContents(document.parseResult.value)) { await interruptAndCheck(cancelToken); this.resolve(node, document); } @@ -108,20 +100,12 @@ export class ZModelLinker extends DefaultLinker { container: AstNode, property: string, document: LangiumDocument, - extraScopes: ScopeProvider[] + extraScopes: ScopeProvider[], ) { - if ( - this.resolveFromScopeProviders( - container, - property, - document, - extraScopes - ) - ) { + if (this.resolveFromScopeProviders(container, property, document, extraScopes)) { return; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any const reference: DefaultReference = (container as any)[property]; this.doLink({ reference, container, property }, document); } @@ -134,20 +118,14 @@ export class ZModelLinker extends DefaultLinker { node: AstNode, property: string, document: LangiumDocument, - providers: ScopeProvider[] + providers: ScopeProvider[], ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const reference: DefaultReference = (node as any)[property]; for (const provider of providers) { const target = provider(reference.$refText); if (target) { reference._ref = target; - reference._nodeDescription = - this.descriptions.createDescription( - target, - target.name, - document - ); + reference._nodeDescription = this.descriptions.createDescription(target, target.name, document); // Add the reference to the document's array of references document.references.push(reference); @@ -158,11 +136,7 @@ export class ZModelLinker extends DefaultLinker { return null; } - private resolve( - node: AstNode, - document: LangiumDocument, - extraScopes: ScopeProvider[] = [] - ) { + private resolve(node: AstNode, document: LangiumDocument, extraScopes: ScopeProvider[] = []) { switch (node.$type) { case StringLiteral: case NumberLiteral: @@ -171,11 +145,7 @@ export class ZModelLinker extends DefaultLinker { break; case InvocationExpr: - this.resolveInvocation( - node as InvocationExpr, - document, - extraScopes - ); + this.resolveInvocation(node as InvocationExpr, document, extraScopes); break; case ArrayExpr: @@ -183,19 +153,11 @@ export class ZModelLinker extends DefaultLinker { break; case ReferenceExpr: - this.resolveReference( - node as ReferenceExpr, - document, - extraScopes - ); + this.resolveReference(node as ReferenceExpr, document, extraScopes); break; case MemberAccessExpr: - this.resolveMemberAccess( - node as MemberAccessExpr, - document, - extraScopes - ); + this.resolveMemberAccess(node as MemberAccessExpr, document, extraScopes); break; case UnaryExpr: @@ -219,11 +181,7 @@ export class ZModelLinker extends DefaultLinker { break; case AttributeArg: - this.resolveAttributeArg( - node as AttributeArg, - document, - extraScopes - ); + this.resolveAttributeArg(node as AttributeArg, document, extraScopes); break; case DataModel: @@ -231,11 +189,7 @@ export class ZModelLinker extends DefaultLinker { break; case DataModelField: - this.resolveDataModelField( - node as DataModelField, - document, - extraScopes - ); + this.resolveDataModelField(node as DataModelField, document, extraScopes); break; default: @@ -244,11 +198,7 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveBinary( - node: BinaryExpr, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveBinary(node: BinaryExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { switch (node.operator) { // TODO: support arithmetics? // case '+': @@ -285,11 +235,7 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveUnary( - node: UnaryExpr, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveUnary(node: UnaryExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { this.resolve(node.operand, document, extraScopes); switch (node.operator) { case '!': @@ -300,45 +246,25 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveObject( - node: ObjectExpr, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { - node.fields.forEach((field) => - this.resolve(field.value, document, extraScopes) - ); + private resolveObject(node: ObjectExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { + node.fields.forEach((field) => this.resolve(field.value, document, extraScopes)); this.resolveToBuiltinTypeOrDecl(node, 'Object'); } - private resolveReference( - node: ReferenceExpr, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveReference(node: ReferenceExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { this.resolveDefault(node, document, extraScopes); if (node.target.ref) { // resolve type if (node.target.ref.$type === EnumField) { - this.resolveToBuiltinTypeOrDecl( - node, - node.target.ref.$container - ); + this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container); } else { - this.resolveToDeclaredType( - node, - (node.target.ref as DataModelField | FunctionParam).type - ); + this.resolveToDeclaredType(node, (node.target.ref as DataModelField | FunctionParam).type); } } } - private resolveArray( - node: ArrayExpr, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveArray(node: ArrayExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { node.items.forEach((item) => this.resolve(item, document, extraScopes)); if (node.items.length > 0) { @@ -351,15 +277,10 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveInvocation( - node: InvocationExpr, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveInvocation(node: InvocationExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { this.linkReference(node, 'function', document, extraScopes); node.args.forEach((arg) => this.resolve(arg, document, extraScopes)); if (node.function.ref) { - // eslint-disable-next-line @typescript-eslint/ban-types const funcDecl = node.function.ref as FunctionDecl; if (isAuthInvocation(node)) { @@ -368,7 +289,7 @@ export class ZModelLinker extends DefaultLinker { // get all data models from loaded and reachable documents const allDecls = getAllLoadedAndReachableDataModelsAndTypeDefs( this.langiumDocuments(), - AstUtils.getContainerOfType(node, isDataModel) + AstUtils.getContainerOfType(node, isDataModel), ); const authDecl = getAuthDecl(allDecls); @@ -399,16 +320,12 @@ export class ZModelLinker extends DefaultLinker { private resolveMemberAccess( node: MemberAccessExpr, document: LangiumDocument, - extraScopes: ScopeProvider[] + extraScopes: ScopeProvider[], ) { this.resolveDefault(node, document, extraScopes); const operandResolved = node.operand.$resolvedType; - if ( - operandResolved && - !operandResolved.array && - isMemberContainer(operandResolved.decl) - ) { + if (operandResolved && !operandResolved.array && isMemberContainer(operandResolved.decl)) { // member access is resolved only in the context of the operand type if (node.member.ref) { this.resolveToDeclaredType(node, node.member.ref.type); @@ -421,30 +338,18 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveCollectionPredicate( - node: BinaryExpr, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveCollectionPredicate(node: BinaryExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { this.resolveDefault(node, document, extraScopes); const resolvedType = node.left.$resolvedType; - if ( - resolvedType && - isMemberContainer(resolvedType.decl) && - resolvedType.array - ) { + if (resolvedType && isMemberContainer(resolvedType.decl) && resolvedType.array) { this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); } else { // error is reported in validation pass } } - private resolveThis( - node: ThisExpr, - _document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveThis(node: ThisExpr, _document: LangiumDocument, extraScopes: ScopeProvider[]) { // resolve from scopes first for (const scope of extraScopes) { const r = scope('this'); @@ -465,27 +370,16 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveNull( - node: NullExpr, - _document: LangiumDocument, - _extraScopes: ScopeProvider[] - ) { + private resolveNull(node: NullExpr, _document: LangiumDocument, _extraScopes: ScopeProvider[]) { // TODO: how to really resolve null? this.resolveToBuiltinTypeOrDecl(node, 'Null'); } - private resolveAttributeArg( - node: AttributeArg, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveAttributeArg(node: AttributeArg, document: LangiumDocument, extraScopes: ScopeProvider[]) { const attrParam = this.findAttrParamForArg(node); const attrAppliedOn = node.$container.$container; - if ( - attrParam?.type.type === 'TransitiveFieldReference' && - isDataModelField(attrAppliedOn) - ) { + if (attrParam?.type.type === 'TransitiveFieldReference' && isDataModelField(attrAppliedOn)) { // "TransitiveFieldReference" is resolved in the context of the containing model of the field // where the attribute is applied // @@ -502,28 +396,17 @@ export class ZModelLinker extends DefaultLinker { // // In model B, the attribute argument "myId" is resolved to the field "myId" in model A - const transitiveDataModel = attrAppliedOn.type.reference - ?.ref as DataModel; + const transitiveDataModel = attrAppliedOn.type.reference?.ref as DataModel; if (transitiveDataModel) { // resolve references in the context of the transitive data model const scopeProvider = (name: string) => - getModelFieldsWithBases(transitiveDataModel).find( - (f) => f.name === name - ); + getModelFieldsWithBases(transitiveDataModel).find((f) => f.name === name); if (isArrayExpr(node.value)) { node.value.items.forEach((item) => { if (isReferenceExpr(item)) { - const resolved = this.resolveFromScopeProviders( - item, - 'target', - document, - [scopeProvider] - ); + const resolved = this.resolveFromScopeProviders(item, 'target', document, [scopeProvider]); if (resolved) { - this.resolveToDeclaredType( - item, - (resolved as DataModelField).type - ); + this.resolveToDeclaredType(item, (resolved as DataModelField).type); } else { // mark unresolvable this.unresolvableRefExpr(item); @@ -531,24 +414,12 @@ export class ZModelLinker extends DefaultLinker { } }); if (node.value.items[0]?.$resolvedType?.decl) { - this.resolveToBuiltinTypeOrDecl( - node.value, - node.value.items[0].$resolvedType.decl, - true - ); + this.resolveToBuiltinTypeOrDecl(node.value, node.value.items[0].$resolvedType.decl, true); } } else if (isReferenceExpr(node.value)) { - const resolved = this.resolveFromScopeProviders( - node.value, - 'target', - document, - [scopeProvider] - ); + const resolved = this.resolveFromScopeProviders(node.value, 'target', document, [scopeProvider]); if (resolved) { - this.resolveToDeclaredType( - node.value, - (resolved as DataModelField).type - ); + this.resolveToDeclaredType(node.value, (resolved as DataModelField).type); } else { // mark unresolvable this.unresolvableRefExpr(node.value); @@ -583,18 +454,14 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveDataModel( - node: DataModel, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveDataModel(node: DataModel, document: LangiumDocument, extraScopes: ScopeProvider[]) { return this.resolveDefault(node, document, extraScopes); } private resolveDataModelField( node: DataModelField, document: LangiumDocument, - extraScopes: ScopeProvider[] + extraScopes: ScopeProvider[], ) { // Field declaration may contain enum references, and enum fields are pushed to the global // scope, so if there're enums with fields with the same name, an arbitrary one will be @@ -627,19 +494,14 @@ export class ZModelLinker extends DefaultLinker { // if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes if (node.type.reference?.ref && isEnum(node.type.reference.ref)) { const contextEnum = node.type.reference.ref as Enum; - const enumScope: ScopeProvider = (name) => - contextEnum.fields.find((f) => f.name === name); + const enumScope: ScopeProvider = (name) => contextEnum.fields.find((f) => f.name === name); scopes = [enumScope, ...scopes]; } this.resolveDefault(node, document, scopes); } - private resolveDefault( - node: AstNode, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { + private resolveDefault(node: AstNode, document: LangiumDocument, extraScopes: ScopeProvider[]) { for (const [property, value] of Object.entries(node)) { if (!property.startsWith('$')) { if (isReference(value)) { @@ -656,10 +518,7 @@ export class ZModelLinker extends DefaultLinker { //#region Utils - private resolveToDeclaredType( - node: AstNode, - type: FunctionParamType | DataModelFieldType | TypeDefFieldType - ) { + private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataModelFieldType | TypeDefFieldType) { let nullable = false; if (isDataModelFieldType(type) || isTypeDefField(type)) { nullable = type.optional; @@ -691,12 +550,7 @@ export class ZModelLinker extends DefaultLinker { } } - private resolveToBuiltinTypeOrDecl( - node: AstNode, - type: ResolvedShape, - array = false, - nullable = false - ) { + private resolveToBuiltinTypeOrDecl(node: AstNode, type: ResolvedShape, array = false, nullable = false) { node.$resolvedType = { decl: type, array, nullable }; } diff --git a/packages/language/src/zmodel-scope.ts b/packages/language/src/zmodel-scope.ts index 7e47d728..c5c2968f 100644 --- a/packages/language/src/zmodel-scope.ts +++ b/packages/language/src/zmodel-scope.ts @@ -51,24 +51,21 @@ export class ZModelScopeComputation extends DefaultScopeComputation { override async computeExports( document: LangiumDocument, - cancelToken?: Cancellation.CancellationToken | undefined + cancelToken?: Cancellation.CancellationToken | undefined, ): Promise { const result = await super.computeExports(document, cancelToken); // add enum fields so they can be globally resolved across modules - for (const node of AstUtils.streamAllContents( - document.parseResult.value - )) { + for (const node of AstUtils.streamAllContents(document.parseResult.value)) { if (cancelToken) { await interruptAndCheck(cancelToken); } if (isEnumField(node)) { - const desc = - this.services.workspace.AstNodeDescriptionProvider.createDescription( - node, - node.name, - document - ); + const desc = this.services.workspace.AstNodeDescriptionProvider.createDescription( + node, + node.name, + document, + ); result.push(desc); } } @@ -76,11 +73,7 @@ export class ZModelScopeComputation extends DefaultScopeComputation { return result; } - override processNode( - node: AstNode, - document: LangiumDocument, - scopes: PrecomputedScopes - ) { + override processNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes) { super.processNode(node, document, scopes); // TODO: merge base // if (isDataModel(node) && !node.$baseMerged) { @@ -106,59 +99,38 @@ export class ZModelScopeProvider extends DefaultScopeProvider { super(services); } - protected override getGlobalScope( - referenceType: string, - context: ReferenceInfo - ): Scope { + protected override getGlobalScope(referenceType: string, context: ReferenceInfo): Scope { const model = AstUtils.getContainerOfType(context.container, isModel); if (!model) { return EMPTY_SCOPE; } - const importedUris = model.imports - .map(resolveImportUri) - .filter((url) => !!url); + const importedUris = model.imports.map(resolveImportUri).filter((url) => !!url); - const importedElements = this.indexManager - .allElements(referenceType) - .filter( - (des) => - // allow current document - UriUtils.equals(des.documentUri, model.$document?.uri) || - // allow stdlib - des.documentUri.path.endsWith(STD_LIB_MODULE_NAME) || - // allow plugin models - des.documentUri.path.endsWith(PLUGIN_MODULE_NAME) || - // allow imported documents - importedUris.some((importedUri) => - UriUtils.equals(des.documentUri, importedUri) - ) - ); + const importedElements = this.indexManager.allElements(referenceType).filter( + (des) => + // allow current document + UriUtils.equals(des.documentUri, model.$document?.uri) || + // allow stdlib + des.documentUri.path.endsWith(STD_LIB_MODULE_NAME) || + // allow plugin models + des.documentUri.path.endsWith(PLUGIN_MODULE_NAME) || + // allow imported documents + importedUris.some((importedUri) => UriUtils.equals(des.documentUri, importedUri)), + ); return new StreamScope(importedElements); } override getScope(context: ReferenceInfo): Scope { - if ( - isMemberAccessExpr(context.container) && - context.container.operand && - context.property === 'member' - ) { + if (isMemberAccessExpr(context.container) && context.container.operand && context.property === 'member') { return this.getMemberAccessScope(context); } - if ( - isReferenceExpr(context.container) && - context.property === 'target' - ) { + if (isReferenceExpr(context.container) && context.property === 'target') { // when reference expression is resolved inside a collection predicate, the scope is the collection - const containerCollectionPredicate = getCollectionPredicateContext( - context.container - ); + const containerCollectionPredicate = getCollectionPredicateContext(context.container); if (containerCollectionPredicate) { - return this.getCollectionPredicateScope( - context, - containerCollectionPredicate as BinaryExpr - ); + return this.getCollectionPredicateScope(context, containerCollectionPredicate as BinaryExpr); } } @@ -181,11 +153,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider { // operand is a reference, it can only be a model/type-def field const ref = operand.target.ref; if (isDataModelField(ref) || isTypeDefField(ref)) { - return this.createScopeForContainer( - ref.type.reference?.ref, - globalScope, - allowTypeDefScope - ); + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } return EMPTY_SCOPE; }) @@ -193,18 +161,10 @@ export class ZModelScopeProvider extends DefaultScopeProvider { // operand is a member access, it must be resolved to a non-array model/typedef type const ref = operand.member.ref; if (isDataModelField(ref) && !ref.type.array) { - return this.createScopeForContainer( - ref.type.reference?.ref, - globalScope, - allowTypeDefScope - ); + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } if (isTypeDefField(ref) && !ref.type.array) { - return this.createScopeForContainer( - ref.type.reference?.ref, - globalScope, - allowTypeDefScope - ); + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } return EMPTY_SCOPE; }) @@ -222,20 +182,14 @@ export class ZModelScopeProvider extends DefaultScopeProvider { if (isFutureInvocation(operand)) { // resolve `future()` to the containing model - return this.createScopeForContainingModel( - node, - globalScope - ); + return this.createScopeForContainingModel(node, globalScope); } return EMPTY_SCOPE; }) .otherwise(() => EMPTY_SCOPE); } - private getCollectionPredicateScope( - context: ReferenceInfo, - collectionPredicate: BinaryExpr - ) { + private getCollectionPredicateScope(context: ReferenceInfo, collectionPredicate: BinaryExpr) { const referenceType = this.reflection.getReferenceType(context); const globalScope = this.getGlobalScope(referenceType, context); const collection = collectionPredicate.left; @@ -250,11 +204,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider { // collection is a reference - model or typedef field const ref = expr.target.ref; if (isDataModelField(ref) || isTypeDefField(ref)) { - return this.createScopeForContainer( - ref.type.reference?.ref, - globalScope, - allowTypeDefScope - ); + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } return EMPTY_SCOPE; }) @@ -262,23 +212,14 @@ export class ZModelScopeProvider extends DefaultScopeProvider { // collection is a member access, it can only be resolved to a model or typedef field const ref = expr.member.ref; if (isDataModelField(ref) || isTypeDefField(ref)) { - return this.createScopeForContainer( - ref.type.reference?.ref, - globalScope, - allowTypeDefScope - ); + return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); } return EMPTY_SCOPE; }) .when(isInvocationExpr, (expr) => { - const returnTypeDecl = - expr.function.ref?.returnType.reference?.ref; + const returnTypeDecl = expr.function.ref?.returnType.reference?.ref; if (isDataModel(returnTypeDecl)) { - return this.createScopeForContainer( - returnTypeDecl, - globalScope, - allowTypeDefScope - ); + return this.createScopeForContainer(returnTypeDecl, globalScope, allowTypeDefScope); } else { return EMPTY_SCOPE; } @@ -298,16 +239,9 @@ export class ZModelScopeProvider extends DefaultScopeProvider { } } - private createScopeForContainer( - node: AstNode | undefined, - globalScope: Scope, - includeTypeDefScope = false - ) { + private createScopeForContainer(node: AstNode | undefined, globalScope: Scope, includeTypeDefScope = false) { if (isDataModel(node)) { - return this.createScopeForNodes( - getModelFieldsWithBases(node), - globalScope - ); + return this.createScopeForNodes(getModelFieldsWithBases(node), globalScope); } else if (includeTypeDefScope && isTypeDef(node)) { return this.createScopeForNodes(node.fields, globalScope); } else { @@ -319,7 +253,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider { // get all data models and type defs from loaded and reachable documents const decls = getAllLoadedAndReachableDataModelsAndTypeDefs( this.services.shared.workspace.LangiumDocuments, - AstUtils.getContainerOfType(node, isDataModel) + AstUtils.getContainerOfType(node, isDataModel), ); const authDecl = getAuthDecl(decls); @@ -334,11 +268,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider { function getCollectionPredicateContext(node: AstNode) { let curr: AstNode | undefined = node; while (curr) { - if ( - curr.$container && - isCollectionPredicate(curr.$container) && - curr.$containerProperty === 'right' - ) { + if (curr.$container && isCollectionPredicate(curr.$container) && curr.$containerProperty === 'right') { return curr.$container; } curr = curr.$container; diff --git a/packages/language/src/zmodel-workspace-manager.ts b/packages/language/src/zmodel-workspace-manager.ts index dda053a0..f21db797 100644 --- a/packages/language/src/zmodel-workspace-manager.ts +++ b/packages/language/src/zmodel-workspace-manager.ts @@ -22,7 +22,7 @@ export class ZModelWorkspaceManager extends DefaultWorkspaceManager { protected override async loadAdditionalDocuments( folders: WorkspaceFolder[], - collector: (document: LangiumDocument) => void + collector: (document: LangiumDocument) => void, ): Promise { await super.loadAdditionalDocuments(folders, collector); @@ -36,28 +36,19 @@ export class ZModelWorkspaceManager extends DefaultWorkspaceManager { const folderPath = this.getRootFolder(folder).fsPath; try { // Try to resolve zenstack from the workspace folder - const languagePackagePath = require.resolve( - '@zenstackhq/language/package.json', - { - paths: [folderPath], - } - ); + const languagePackagePath = require.resolve('@zenstackhq/language/package.json', { + paths: [folderPath], + }); const languagePackageDir = path.dirname(languagePackagePath); - const candidateStdlibPath = path.join( - languagePackageDir, - 'res', - STD_LIB_MODULE_NAME - ); + const candidateStdlibPath = path.join(languagePackageDir, 'res', STD_LIB_MODULE_NAME); // Check if the stdlib file exists in the installed package if (fs.existsSync(candidateStdlibPath)) { installedStdlibPath = candidateStdlibPath; - console.log( - `Found installed zenstack package stdlib at: ${installedStdlibPath}` - ); + console.log(`Found installed zenstack package stdlib at: ${installedStdlibPath}`); break; } - } catch (error) { + } catch { // Package not found or other error, continue to next folder continue; } @@ -69,9 +60,7 @@ export class ZModelWorkspaceManager extends DefaultWorkspaceManager { // Fallback to bundled stdlib // isomorphic __dirname const _dirname = - typeof __dirname !== 'undefined' - ? __dirname - : path.dirname(fileURLToPath(import.meta.url)); + typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); stdLibPath = path.join(_dirname, '../res', STD_LIB_MODULE_NAME); console.log(`Using bundled stdlib in extension:`, stdLibPath); diff --git a/packages/runtime/eslint.config.js b/packages/runtime/eslint.config.js new file mode 100644 index 00000000..f04dc9fe --- /dev/null +++ b/packages/runtime/eslint.config.js @@ -0,0 +1,9 @@ +import config from '@zenstackhq/eslint-config/base.js'; +import tseslint from 'typescript-eslint'; + +/** @type {import("eslint").Linter.Config} */ +export default tseslint.config(config, { + rules: { + '@typescript-eslint/no-unused-expressions': 'off', + }, +}); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index df94eaaa..48108f49 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -97,6 +97,7 @@ "@zenstackhq/sdk": "workspace:*", "@zenstackhq/testtools": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*", "tmp": "^0.2.3", "tsx": "^4.19.2" } diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index 84307e1f..90831277 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -41,7 +41,7 @@ import { ResultProcessor } from './result-processor'; export const ZenStackClient = function ( this: any, schema: any, - options: ClientOptions + options: ClientOptions, ) { return new ClientImpl(schema, options); } as unknown as ClientConstructor; @@ -57,7 +57,7 @@ export class ClientImpl { constructor( private readonly schema: Schema, private options: ClientOptions, - baseClient?: ClientImpl + baseClient?: ClientImpl, ) { this.$schema = schema; this.$options = options ?? ({} as ClientOptions); @@ -76,26 +76,17 @@ export class ClientImpl { baseClient.kyselyProps.driver as ZenStackDriver, baseClient.kyselyProps.dialect.createQueryCompiler(), baseClient.kyselyProps.dialect.createAdapter(), - new DefaultConnectionProvider(baseClient.kyselyProps.driver) + new DefaultConnectionProvider(baseClient.kyselyProps.driver), ), }; this.kyselyRaw = baseClient.kyselyRaw; } else { const dialect = this.getKyselyDialect(); - const driver = new ZenStackDriver( - dialect.createDriver(), - new Log(this.$options.log ?? []) - ); + const driver = new ZenStackDriver(dialect.createDriver(), new Log(this.$options.log ?? [])); const compiler = dialect.createQueryCompiler(); const adapter = dialect.createAdapter(); const connectionProvider = new DefaultConnectionProvider(driver); - const executor = new ZenStackQueryExecutor( - this, - driver, - compiler, - adapter, - connectionProvider - ); + const executor = new ZenStackQueryExecutor(this, driver, compiler, adapter, connectionProvider); this.kyselyProps = { config: { @@ -110,12 +101,7 @@ export class ClientImpl { // raw kysely instance with default executor this.kyselyRaw = new Kysely({ ...this.kyselyProps, - executor: new DefaultQueryExecutor( - compiler, - adapter, - connectionProvider, - [] - ), + executor: new DefaultQueryExecutor(compiler, adapter, connectionProvider, []), }); } @@ -140,20 +126,14 @@ export class ClientImpl { } private makePostgresKyselyDialect(): PostgresDialect { - return new PostgresDialect( - this.options.dialectConfig as PostgresDialectConfig - ); + return new PostgresDialect(this.options.dialectConfig as PostgresDialectConfig); } private makeSqliteKyselyDialect(): SqliteDialect { - return new SqliteDialect( - this.options.dialectConfig as SqliteDialectConfig - ); + return new SqliteDialect(this.options.dialectConfig as SqliteDialectConfig); } - async $transaction( - callback: (tx: ClientContract) => Promise - ): Promise { + async $transaction(callback: (tx: ClientContract) => Promise): Promise { return this.kysely.transaction().execute((tx) => { const txClient = new ClientImpl(this.schema, this.$options); txClient.kysely = tx; @@ -162,24 +142,15 @@ export class ClientImpl { } get $procedures() { - return Object.keys(this.$schema.procedures ?? {}).reduce( - (acc, name) => { - acc[name] = (...args: unknown[]) => this.handleProc(name, args); - return acc; - }, - {} as any - ); + return Object.keys(this.$schema.procedures ?? {}).reduce((acc, name) => { + acc[name] = (...args: unknown[]) => this.handleProc(name, args); + return acc; + }, {} as any); } private async handleProc(name: string, args: unknown[]) { - if ( - !('procedures' in this.$options) || - !this.$options || - typeof this.$options.procedures !== 'object' - ) { - throw new QueryError( - 'Procedures are not configured for the client.' - ); + if (!('procedures' in this.$options) || !this.$options || typeof this.$options.procedures !== 'object') { + throw new QueryError('Procedures are not configured for the client.'); } const procOptions = this.$options.procedures as ProceduresOptions< @@ -188,12 +159,9 @@ export class ClientImpl { } >; if (!procOptions[name] || typeof procOptions[name] !== 'function') { - throw new Error( - `Procedure "${name}" does not have a handler configured.` - ); + throw new Error(`Procedure "${name}" does not have a handler configured.`); } - // eslint-disable-next-line @typescript-eslint/ban-types return (procOptions[name] as Function).apply(this, [this, ...args]); } @@ -225,11 +193,7 @@ export class ClientImpl { if (auth !== undefined && typeof auth !== 'object') { throw new Error('Invalid auth object'); } - const newClient = new ClientImpl( - this.schema, - this.$options, - this - ); + const newClient = new ClientImpl(this.schema, this.$options, this); newClient.auth = auth; return newClient; } @@ -239,9 +203,7 @@ export class ClientImpl { } } -function createClientProxy( - client: ClientContract -): ClientImpl { +function createClientProxy(client: ClientContract): ClientImpl { const inputValidator = new InputValidator(client.$schema); const resultProcessor = new ResultProcessor(client.$schema); @@ -252,16 +214,9 @@ function createClientProxy( } if (typeof prop === 'string') { - const model = Object.keys(client.$schema.models).find( - (m) => m.toLowerCase() === prop.toLowerCase() - ); + const model = Object.keys(client.$schema.models).find((m) => m.toLowerCase() === prop.toLowerCase()); if (model) { - return createModelCrudHandler( - client, - model as GetModels, - inputValidator, - resultProcessor - ); + return createModelCrudHandler(client, model as GetModels, inputValidator, resultProcessor); } } @@ -270,27 +225,21 @@ function createClientProxy( }) as unknown as ClientImpl; } -function createModelCrudHandler< - Schema extends SchemaDef, - Model extends GetModels ->( +function createModelCrudHandler>( client: ClientContract, model: Model, inputValidator: InputValidator, - resultProcessor: ResultProcessor + resultProcessor: ResultProcessor, ): ModelOperations { const createPromise = ( operation: CrudOperation, args: unknown, handler: BaseOperationHandler, postProcess = false, - throwIfNoResult = false + throwIfNoResult = false, ) => { return createDeferredPromise(async () => { - let proceed = async ( - _args?: unknown, - tx?: ClientContract - ) => { + let proceed = async (_args?: unknown, tx?: ClientContract) => { const _handler = tx ? handler.withClient(tx) : handler; const r = await _handler.handle(operation, _args ?? args); if (!r && throwIfNoResult) { @@ -316,8 +265,7 @@ function createModelCrudHandler< for (const plugin of plugins) { if (plugin.onQuery) { const _proceed = proceed; - proceed = () => - plugin.onQuery!({ ...context, proceed: _proceed }); + proceed = () => plugin.onQuery!({ ...context, proceed: _proceed }); } } @@ -327,12 +275,7 @@ function createModelCrudHandler< return { findUnique: (args: unknown) => { - return createPromise( - 'findUnique', - args, - new FindOperationHandler(client, model, inputValidator), - true - ); + return createPromise('findUnique', args, new FindOperationHandler(client, model, inputValidator), true); }, findUniqueOrThrow: (args: unknown) => { @@ -341,17 +284,12 @@ function createModelCrudHandler< args, new FindOperationHandler(client, model, inputValidator), true, - true + true, ); }, findFirst: (args: unknown) => { - return createPromise( - 'findFirst', - args, - new FindOperationHandler(client, model, inputValidator), - true - ); + return createPromise('findFirst', args, new FindOperationHandler(client, model, inputValidator), true); }, findFirstOrThrow: (args: unknown) => { @@ -360,35 +298,20 @@ function createModelCrudHandler< args, new FindOperationHandler(client, model, inputValidator), true, - true + true, ); }, findMany: (args: unknown) => { - return createPromise( - 'findMany', - args, - new FindOperationHandler(client, model, inputValidator), - true - ); + return createPromise('findMany', args, new FindOperationHandler(client, model, inputValidator), true); }, create: (args: unknown) => { - return createPromise( - 'create', - args, - new CreateOperationHandler(client, model, inputValidator), - true - ); + return createPromise('create', args, new CreateOperationHandler(client, model, inputValidator), true); }, createMany: (args: unknown) => { - return createPromise( - 'createMany', - args, - new CreateOperationHandler(client, model, inputValidator), - false - ); + return createPromise('createMany', args, new CreateOperationHandler(client, model, inputValidator), false); }, createManyAndReturn: (args: unknown) => { @@ -396,26 +319,16 @@ function createModelCrudHandler< 'createManyAndReturn', args, new CreateOperationHandler(client, model, inputValidator), - true + true, ); }, update: (args: unknown) => { - return createPromise( - 'update', - args, - new UpdateOperationHandler(client, model, inputValidator), - true - ); + return createPromise('update', args, new UpdateOperationHandler(client, model, inputValidator), true); }, updateMany: (args: unknown) => { - return createPromise( - 'updateMany', - args, - new UpdateOperationHandler(client, model, inputValidator), - false - ); + return createPromise('updateMany', args, new UpdateOperationHandler(client, model, inputValidator), false); }, updateManyAndReturn: (args: unknown) => { @@ -423,44 +336,24 @@ function createModelCrudHandler< 'updateManyAndReturn', args, new UpdateOperationHandler(client, model, inputValidator), - true + true, ); }, upsert: (args: unknown) => { - return createPromise( - 'upsert', - args, - new UpdateOperationHandler(client, model, inputValidator), - true - ); + return createPromise('upsert', args, new UpdateOperationHandler(client, model, inputValidator), true); }, delete: (args: unknown) => { - return createPromise( - 'delete', - args, - new DeleteOperationHandler(client, model, inputValidator), - true - ); + return createPromise('delete', args, new DeleteOperationHandler(client, model, inputValidator), true); }, deleteMany: (args: unknown) => { - return createPromise( - 'deleteMany', - args, - new DeleteOperationHandler(client, model, inputValidator), - false - ); + return createPromise('deleteMany', args, new DeleteOperationHandler(client, model, inputValidator), false); }, count: (args: unknown) => { - return createPromise( - 'count', - args, - new CountOperationHandler(client, model, inputValidator), - false - ); + return createPromise('count', args, new CountOperationHandler(client, model, inputValidator), false); }, aggregate: (args: unknown) => { @@ -468,16 +361,12 @@ function createModelCrudHandler< 'aggregate', args, new AggregateOperationHandler(client, model, inputValidator), - false + false, ); }, groupBy: (args: unknown) => { - return createPromise( - 'groupBy', - args, - new GroupByeOperationHandler(client, model, inputValidator) - ); + return createPromise('groupBy', args, new GroupByeOperationHandler(client, model, inputValidator)); }, } as ModelOperations; } diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts index c047e42d..0805029b 100644 --- a/packages/runtime/src/client/contract.ts +++ b/packages/runtime/src/client/contract.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-types */ - import { type GetModels, type ProcedureDef, type SchemaDef } from '../schema'; import type { Decimal } from 'decimal.js'; import type { AuthType } from '../schema/auth'; @@ -43,9 +41,7 @@ export type ClientContract = { /** * Starts a transaction. */ - $transaction( - callback: (tx: ClientContract) => Promise - ): Promise; + $transaction(callback: (tx: ClientContract) => Promise): Promise; /** * Returns a new client with the specified plugin installed. @@ -68,55 +64,43 @@ export type ClientContract = { */ $pushSchema(): Promise; } & { - [Key in GetModels as Uncapitalize]: ModelOperations< - Schema, - Key - >; + [Key in GetModels as Uncapitalize]: ModelOperations; } & Procedures; type MapType = T extends 'String' ? string : T extends 'Int' - ? number - : T extends 'Float' - ? number - : T extends 'BigInt' - ? bigint - : T extends 'Decimal' - ? Decimal - : T extends 'Boolean' - ? boolean - : T extends 'DateTime' - ? Date - : T extends GetModels - ? ModelResult - : unknown; + ? number + : T extends 'Float' + ? number + : T extends 'BigInt' + ? bigint + : T extends 'Decimal' + ? Decimal + : T extends 'Boolean' + ? boolean + : T extends 'DateTime' + ? Date + : T extends GetModels + ? ModelResult + : unknown; export type Procedures = Schema['procedures'] extends Record ? { $procedures: { - [Key in keyof Schema['procedures']]: ProcedureFunc< - Schema, - Schema['procedures'][Key] - >; + [Key in keyof Schema['procedures']]: ProcedureFunc; }; } : {}; -export type ProcedureFunc< - Schema extends SchemaDef, - Proc extends ProcedureDef -> = ( +export type ProcedureFunc = ( ...args: MapProcedureParams ) => Promise>; type MapProcedureParams = { [P in keyof Params]: Params[P] extends { type: infer U } - ? OrUndefinedIf< - MapType, - Params[P] extends { optional: true } ? true : false - > + ? OrUndefinedIf, Params[P] extends { optional: true } ? true : false> : never; }; @@ -125,12 +109,9 @@ type MapProcedureParams = { */ export interface ClientConstructor { new ( - schema: HasComputedFields extends false ? Schema : never - ): ClientContract; - new ( - schema: Schema, - options: ClientOptions + schema: HasComputedFields extends false ? Schema : never, ): ClientContract; + new (schema: Schema, options: ClientOptions): ClientContract; } /** diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index 4411b734..b78038ac 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-types */ - import type { ExpressionBuilder, OperandExpression, SqlBool } from 'kysely'; import type { Optional } from 'utility-types'; import type { @@ -45,7 +43,7 @@ type DefaultModelResult< Model extends GetModels, Omit = undefined, Optional = false, - Array = false + Array = false, > = WrapType< { [Key in NonRelationFields as Key extends keyof Omit @@ -58,74 +56,59 @@ type DefaultModelResult< Array >; -type ModelSelectResult< - Schema extends SchemaDef, - Model extends GetModels, - Select, - Omit -> = { +type ModelSelectResult, Select, Omit> = { [Key in keyof Select as Select[Key] extends false | undefined ? never : Key extends keyof Omit - ? Omit[Key] extends true - ? never - : Key - : Key extends '_count' - ? Select[Key] extends SelectCount - ? Key - : never - : Key]: Key extends '_count' + ? Omit[Key] extends true + ? never + : Key + : Key extends '_count' + ? Select[Key] extends SelectCount + ? Key + : never + : Key]: Key extends '_count' ? SelectCountResult : Key extends NonRelationFields - ? MapFieldType - : Key extends RelationFields - ? Select[Key] extends FindArgs< - Schema, - RelationFieldType, - FieldIsArray - > - ? ModelResult< + ? MapFieldType + : Key extends RelationFields + ? Select[Key] extends FindArgs< Schema, RelationFieldType, - Select[Key], - FieldIsOptional, FieldIsArray > - : DefaultModelResult< - Schema, - RelationFieldType, - Omit, - FieldIsOptional, - FieldIsArray - > - : never; + ? ModelResult< + Schema, + RelationFieldType, + Select[Key], + FieldIsOptional, + FieldIsArray + > + : DefaultModelResult< + Schema, + RelationFieldType, + Omit, + FieldIsOptional, + FieldIsArray + > + : never; }; -type SelectCountResult< - Schema extends SchemaDef, - Model extends GetModels, - C -> = C extends true +type SelectCountResult, C> = C extends true ? { // count all to-many relation fields - [Key in RelationFields as FieldIsArray< - Schema, - Model, - Key - > extends true - ? Key - : never]: number; + [Key in RelationFields as FieldIsArray extends true ? Key : never]: number; } : C extends { select: infer S } - ? { [Key in keyof S]: number } - : never; + ? { [Key in keyof S]: number } + : never; export type ModelResult< Schema extends SchemaDef, Model extends GetModels, Args extends SelectIncludeOmit = {}, Optional = false, - Array = false + Array = false, > = WrapType< Args extends { select: infer S; @@ -133,37 +116,35 @@ export type ModelResult< } ? ModelSelectResult : Args extends { - include: infer I; - omit?: infer O; - } - ? DefaultModelResult & { - [Key in keyof I & RelationFields as I[Key] extends - | false - | undefined - ? never - : Key]: I[Key] extends FindArgs< - Schema, - RelationFieldType, - FieldIsArray - > - ? ModelResult< - Schema, - RelationFieldType, - I[Key], - FieldIsOptional, - FieldIsArray - > - : DefaultModelResult< - Schema, - RelationFieldType, - undefined, - FieldIsOptional, - FieldIsArray - >; - } - : Args extends { omit: infer O } - ? DefaultModelResult - : DefaultModelResult, + include: infer I; + omit?: infer O; + } + ? DefaultModelResult & { + [Key in keyof I & RelationFields as I[Key] extends false | undefined + ? never + : Key]: I[Key] extends FindArgs< + Schema, + RelationFieldType, + FieldIsArray + > + ? ModelResult< + Schema, + RelationFieldType, + I[Key], + FieldIsOptional, + FieldIsArray + > + : DefaultModelResult< + Schema, + RelationFieldType, + undefined, + FieldIsOptional, + FieldIsArray + >; + } + : Args extends { omit: infer O } + ? DefaultModelResult + : DefaultModelResult, Optional, Array >; @@ -177,7 +158,7 @@ export type BatchResult = { count: number }; export type WhereInput< Schema extends SchemaDef, Model extends GetModels, - ScalarOnly extends boolean = false + ScalarOnly extends boolean = false, > = { [Key in GetFields as ScalarOnly extends true ? Key extends RelationFields @@ -187,34 +168,21 @@ export type WhereInput< ? // relation RelationFilter : // enum - GetFieldType extends GetEnums - ? EnumFilter< - Schema, - GetFieldType, - FieldIsOptional - > - : FieldIsArray extends true - ? ArrayFilter> - : // primitive - PrimitiveFilter< - GetFieldType, - FieldIsOptional - >; + GetFieldType extends GetEnums + ? EnumFilter, FieldIsOptional> + : FieldIsArray extends true + ? ArrayFilter> + : // primitive + PrimitiveFilter, FieldIsOptional>; } & { - $expr?: ( - eb: ExpressionBuilder, Model> - ) => OperandExpression; + $expr?: (eb: ExpressionBuilder, Model>) => OperandExpression; } & { AND?: OrArray>; OR?: WhereInput[]; NOT?: OrArray>; }; -type EnumFilter< - Schema extends SchemaDef, - T extends GetEnums, - Nullable extends boolean -> = +type EnumFilter, Nullable extends boolean> = | NullableIf, Nullable> | { equals?: NullableIf, Nullable>; @@ -231,26 +199,19 @@ type ArrayFilter = { isEmpty?: boolean; }; -type PrimitiveFilter< - T extends string, - Nullable extends boolean -> = T extends 'String' +type PrimitiveFilter = T extends 'String' ? StringFilter : T extends 'Int' | 'Float' | 'Decimal' | 'BigInt' - ? NumberFilter - : T extends 'Boolean' - ? BooleanFilter - : T extends 'DateTime' - ? DateTimeFilter - : T extends 'Json' - ? 'Not implemented yet' // TODO: Json filter - : never; - -export type CommonPrimitiveFilter< - DataType, - T extends BuiltinType, - Nullable extends boolean -> = { + ? NumberFilter + : T extends 'Boolean' + ? BooleanFilter + : T extends 'DateTime' + ? DateTimeFilter + : T extends 'Json' + ? 'Not implemented yet' // TODO: Json filter + : never; + +export type CommonPrimitiveFilter = { equals?: NullableIf; in?: DataType[]; notIn?: DataType[]; @@ -270,10 +231,7 @@ export type StringFilter = mode?: 'default' | 'insensitive'; }); -export type NumberFilter< - T extends 'Int' | 'Float' | 'Decimal' | 'BigInt', - Nullable extends boolean -> = +export type NumberFilter = | NullableIf | CommonPrimitiveFilter; @@ -303,13 +261,9 @@ export type OrderBy< Schema extends SchemaDef, Model extends GetModels, WithRelation extends boolean, - WithAggregation extends boolean + WithAggregation extends boolean, > = { - [Key in NonRelationFields]?: FieldIsOptional< - Schema, - Model, - Key - > extends true + [Key in NonRelationFields]?: FieldIsOptional extends true ? | SortOrder | { @@ -319,20 +273,11 @@ export type OrderBy< : SortOrder; } & (WithRelation extends true ? { - [Key in RelationFields]?: FieldIsArray< - Schema, - Model, - Key - > extends true + [Key in RelationFields]?: FieldIsArray extends true ? { _count?: SortOrder; } - : OrderBy< - Schema, - RelationFieldType, - WithRelation, - WithAggregation - >; + : OrderBy, WithRelation, WithAggregation>; } : {}) & (WithAggregation extends true @@ -348,32 +293,20 @@ export type OrderBy< }) : {}); -export type WhereUniqueInput< - Schema extends SchemaDef, - Model extends GetModels -> = AtLeast< +export type WhereUniqueInput> = AtLeast< { [Key in keyof GetModel['uniqueFields']]?: GetModel< Schema, Model >['uniqueFields'][Key] extends Pick - ? MapFieldDefType< - Schema, - GetModel['uniqueFields'][Key] - > + ? MapFieldDefType['uniqueFields'][Key]> : // multi-field unique { - [Key1 in keyof GetModel< - Schema, - Model - >['uniqueFields'][Key]]: GetModel< + [Key1 in keyof GetModel['uniqueFields'][Key]]: GetModel< Schema, Model >['uniqueFields'][Key][Key1] extends Pick - ? MapFieldDefType< - Schema, - GetModel['uniqueFields'][Key][Key1] - > + ? MapFieldDefType['uniqueFields'][Key][Key1]> : never; }; } & WhereInput, @@ -384,11 +317,7 @@ type OmitFields> = { [Key in NonRelationFields]?: true; }; -export type SelectIncludeOmit< - Schema extends SchemaDef, - Model extends GetModels, - AllowCount extends boolean -> = { +export type SelectIncludeOmit, AllowCount extends boolean> = { select?: Select; include?: Include; omit?: OmitFields; @@ -405,8 +334,8 @@ type Cursor> = { type Select< Schema extends SchemaDef, Model extends GetModels, - AllowCount extends Boolean, - AllowRelation extends boolean = true + AllowCount extends boolean, + AllowRelation extends boolean = true, > = { [Key in NonRelationFields]?: true; } & (AllowRelation extends true ? Include : {}) & // relation fields @@ -417,20 +346,10 @@ type SelectCount> = | true | { select: { - [Key in RelationFields as FieldIsArray< - Schema, - Model, - Key - > extends true - ? Key - : never]: + [Key in RelationFields as FieldIsArray extends true ? Key : never]: | true | { - where: WhereInput< - Schema, - RelationFieldType, - false - >; + where: WhereInput, false>; }; }; }; @@ -446,8 +365,8 @@ type Include> = { FieldIsArray extends true ? true : FieldIsOptional extends true - ? true - : false + ? true + : false >; }; @@ -460,13 +379,13 @@ export type SelectSubset = { } & (T extends { select: any; include: any } ? 'Please either choose `select` or `include`.' : T extends { select: any; omit: any } - ? 'Please either choose `select` or `omit`.' - : {}); + ? 'Please either choose `select` or `omit`.' + : {}); type ToManyRelationFilter< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = { every?: WhereInput>; some?: WhereInput>; @@ -476,7 +395,7 @@ type ToManyRelationFilter< type ToOneRelationFilter< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = NullableIf< WhereInput> & { is?: NullableIf< @@ -494,10 +413,11 @@ type ToOneRelationFilter< type RelationFilter< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = FieldIsArray extends true - ? ToManyRelationFilter - : ToOneRelationFilter; + Field extends RelationFields, +> = + FieldIsArray extends true + ? ToManyRelationFilter + : ToOneRelationFilter; //#endregion @@ -506,75 +426,53 @@ type RelationFilter< export type MapFieldType< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = MapFieldDefType>; -type MapFieldDefType< - Schema extends SchemaDef, - T extends Pick -> = WrapType< - T['type'] extends GetEnums - ? keyof GetEnum - : MapBaseType, +type MapFieldDefType> = WrapType< + T['type'] extends GetEnums ? keyof GetEnum : MapBaseType, T['optional'], T['array'] >; -export type OptionalFieldsForCreate< - Schema extends SchemaDef, - Model extends GetModels -> = keyof { - [Key in GetFields as FieldIsOptional< - Schema, - Model, - Key - > extends true +export type OptionalFieldsForCreate> = keyof { + [Key in GetFields as FieldIsOptional extends true ? Key : FieldHasDefault extends true - ? Key - : GetField['updatedAt'] extends true - ? Key - : FieldIsRelationArray extends true - ? Key - : never]: GetField; + ? Key + : GetField['updatedAt'] extends true + ? Key + : FieldIsRelationArray extends true + ? Key + : never]: GetField; }; type GetRelation< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = GetField['relation']; type OppositeRelation< Schema extends SchemaDef, Model extends GetModels, Field extends GetFields, - FT = FieldType -> = FT extends GetModels - ? GetRelation extends RelationInfo - ? GetRelation['opposite'] extends GetFields< - Schema, - FT - > - ? Schema['models'][FT]['fields'][GetRelation< - Schema, - Model, - Field - >['opposite']]['relation'] + FT = FieldType, +> = + FT extends GetModels + ? GetRelation extends RelationInfo + ? GetRelation['opposite'] extends GetFields + ? Schema['models'][FT]['fields'][GetRelation['opposite']]['relation'] + : never : never - : never - : never; + : never; export type OppositeRelationFields< Schema extends SchemaDef, Model extends GetModels, Field extends GetFields, - Opposite = OppositeRelation -> = Opposite extends RelationInfo - ? Opposite['fields'] extends string[] - ? Opposite['fields'] - : [] - : []; + Opposite = OppositeRelation, +> = Opposite extends RelationInfo ? (Opposite['fields'] extends string[] ? Opposite['fields'] : []) : []; export type OppositeRelationAndFK< Schema extends SchemaDef, @@ -582,12 +480,13 @@ export type OppositeRelationAndFK< Field extends GetFields, FT = FieldType, Relation = GetField['relation'], - Opposite = Relation extends RelationInfo ? Relation['opposite'] : never -> = FT extends GetModels - ? Opposite extends GetFields - ? Opposite | OppositeRelationFields[number] - : never - : never; + Opposite = Relation extends RelationInfo ? Relation['opposite'] : never, +> = + FT extends GetModels + ? Opposite extends GetFields + ? Opposite | OppositeRelationFields[number] + : never + : never; //#endregion @@ -597,7 +496,7 @@ export type FindArgs< Schema extends SchemaDef, Model extends GetModels, Collection extends boolean, - AllowFilter extends boolean = true + AllowFilter extends boolean = true, > = (Collection extends true ? { skip?: number; @@ -614,10 +513,7 @@ export type FindArgs< Distinct & Cursor; -export type FindUniqueArgs< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type FindUniqueArgs> = { where?: WhereUniqueInput; } & SelectIncludeOmit; @@ -625,54 +521,43 @@ export type FindUniqueArgs< //#region Create args -export type CreateArgs< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type CreateArgs> = { data: CreateInput; select?: Select; include?: Include; omit?: OmitFields; }; -export type CreateManyArgs< - Schema extends SchemaDef, - Model extends GetModels -> = CreateManyPayload; +export type CreateManyArgs> = CreateManyPayload< + Schema, + Model +>; -export type CreateManyAndReturnArgs< - Schema extends SchemaDef, - Model extends GetModels -> = CreateManyPayload & { +export type CreateManyAndReturnArgs> = CreateManyPayload< + Schema, + Model +> & { select?: Select; omit?: OmitFields; }; -type OptionalWrap< - Schema extends SchemaDef, - Model extends GetModels, - T extends object -> = Optional>; +type OptionalWrap, T extends object> = Optional< + T, + keyof T & OptionalFieldsForCreate +>; -type CreateScalarPayload< - Schema extends SchemaDef, - Model extends GetModels -> = OptionalWrap< +type CreateScalarPayload> = OptionalWrap< Schema, Model, { - [Key in ScalarFields]: ScalarCreatePayload< - Schema, - Model, - Key - >; + [Key in ScalarFields]: ScalarCreatePayload; } >; type ScalarCreatePayload< Schema extends SchemaDef, Model extends GetModels, - Field extends ScalarFields + Field extends ScalarFields, > = | MapFieldType | (FieldIsArray extends true @@ -681,25 +566,18 @@ type ScalarCreatePayload< } : never); -type CreateFKPayload< - Schema extends SchemaDef, - Model extends GetModels -> = OptionalWrap< +type CreateFKPayload> = OptionalWrap< Schema, Model, { - [Key in ForeignKeyFields]: MapFieldType< - Schema, - Model, - Key - >; + [Key in ForeignKeyFields]: MapFieldType; } >; type CreateRelationFieldPayload< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = Omit< { create?: NestedCreateInput; @@ -711,92 +589,63 @@ type CreateRelationFieldPayload< FieldIsArray extends true ? never : 'createMany' >; -type CreateRelationPayload< - Schema extends SchemaDef, - Model extends GetModels -> = OptionalWrap< +type CreateRelationPayload> = OptionalWrap< Schema, Model, { - [Key in RelationFields]: CreateRelationFieldPayload< - Schema, - Model, - Key - >; + [Key in RelationFields]: CreateRelationFieldPayload; } >; -type CreateWithFKInput< - Schema extends SchemaDef, - Model extends GetModels -> = CreateScalarPayload & CreateFKPayload; +type CreateWithFKInput> = CreateScalarPayload & + CreateFKPayload; -type CreateWithRelationInput< - Schema extends SchemaDef, - Model extends GetModels -> = CreateScalarPayload & CreateRelationPayload; +type CreateWithRelationInput> = CreateScalarPayload< + Schema, + Model +> & + CreateRelationPayload; type ConnectOrCreatePayload< Schema extends SchemaDef, Model extends GetModels, - Without extends string = never + Without extends string = never, > = { where: WhereUniqueInput; create: CreateInput; }; -type CreateManyPayload< - Schema extends SchemaDef, - Model extends GetModels, - Without extends string = never -> = { - data: OrArray< - Omit, Without> & - Omit, Without> - >; +type CreateManyPayload, Without extends string = never> = { + data: OrArray, Without> & Omit, Without>>; skipDuplicates?: boolean; }; export type CreateInput< Schema extends SchemaDef, Model extends GetModels, - Without extends string = never -> = XOR< - Omit, Without>, - Omit, Without> ->; + Without extends string = never, +> = XOR, Without>, Omit, Without>>; type NestedCreateInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = OrArray< - CreateInput< - Schema, - RelationFieldType, - OppositeRelationAndFK - >, + CreateInput, OppositeRelationAndFK>, FieldIsArray >; type NestedCreateManyInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = CreateManyPayload< - Schema, - RelationFieldType, - OppositeRelationAndFK ->; + Field extends RelationFields, +> = CreateManyPayload, OppositeRelationAndFK>; //#endregion // #region Update args -export type UpdateArgs< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type UpdateArgs> = { data: UpdateInput; where: WhereUniqueInput; select?: Select; @@ -804,33 +653,26 @@ export type UpdateArgs< omit?: OmitFields; }; -export type UpdateManyArgs< - Schema extends SchemaDef, - Model extends GetModels -> = UpdateManyPayload; +export type UpdateManyArgs> = UpdateManyPayload< + Schema, + Model +>; -export type UpdateManyAndReturnArgs< - Schema extends SchemaDef, - Model extends GetModels -> = UpdateManyPayload & { +export type UpdateManyAndReturnArgs> = UpdateManyPayload< + Schema, + Model +> & { select?: Select; omit?: OmitFields; }; -type UpdateManyPayload< - Schema extends SchemaDef, - Model extends GetModels, - Without extends string = never -> = { +type UpdateManyPayload, Without extends string = never> = { data: OrArray>; where?: WhereInput; limit?: number; }; -export type UpsertArgs< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type UpsertArgs> = { create: CreateInput; update: UpdateInput; where: WhereUniqueInput; @@ -842,14 +684,10 @@ export type UpsertArgs< export type UpdateScalarInput< Schema extends SchemaDef, Model extends GetModels, - Without extends string = never + Without extends string = never, > = Omit< { - [Key in NonRelationFields]?: ScalarUpdatePayload< - Schema, - Model, - Key - >; + [Key in NonRelationFields]?: ScalarUpdatePayload; }, Without >; @@ -857,7 +695,7 @@ export type UpdateScalarInput< type ScalarUpdatePayload< Schema extends SchemaDef, Model extends GetModels, - Field extends NonRelationFields + Field extends NonRelationFields, > = | MapFieldType | (Field extends NumericFields @@ -879,14 +717,10 @@ type ScalarUpdatePayload< export type UpdateRelationInput< Schema extends SchemaDef, Model extends GetModels, - Without extends string = never + Without extends string = never, > = Omit< { - [Key in RelationFields]?: UpdateRelationFieldPayload< - Schema, - Model, - Key - >; + [Key in RelationFields]?: UpdateRelationFieldPayload; }, Without >; @@ -894,22 +728,22 @@ export type UpdateRelationInput< export type UpdateInput< Schema extends SchemaDef, Model extends GetModels, - Without extends string = never -> = UpdateScalarInput & - UpdateRelationInput; + Without extends string = never, +> = UpdateScalarInput & UpdateRelationInput; type UpdateRelationFieldPayload< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = FieldIsArray extends true - ? ToManyRelationUpdateInput - : ToOneRelationUpdateInput; + Field extends RelationFields, +> = + FieldIsArray extends true + ? ToManyRelationUpdateInput + : ToOneRelationUpdateInput; type ToManyRelationUpdateInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = { create?: NestedCreateInput; createMany?: NestedCreateManyInput; @@ -927,7 +761,7 @@ type ToManyRelationUpdateInput< type ToOneRelationUpdateInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = { create?: NestedCreateInput; connect?: ConnectInput; @@ -945,20 +779,14 @@ type ToOneRelationUpdateInput< // #region Delete args -export type DeleteArgs< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type DeleteArgs> = { where: WhereUniqueInput; select?: Select; include?: Include; omit?: OmitFields; }; -export type DeleteManyArgs< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type DeleteManyArgs> = { where?: WhereInput; limit?: number; }; @@ -967,27 +795,21 @@ export type DeleteManyArgs< // #region Count args -export type CountArgs< - Schema extends SchemaDef, - Model extends GetModels -> = Omit< +export type CountArgs> = Omit< FindArgs, 'select' | 'include' | 'distinct' | 'omit' > & { select?: CountAggregateInput | true; }; -export type CountAggregateInput< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type CountAggregateInput> = { [Key in NonRelationFields]?: true; } & { _all?: true }; export type CountResult< Schema extends SchemaDef, Model extends GetModels, - Args extends CountArgs + Args extends CountArgs, > = Args extends { select: infer S } ? S extends true ? number @@ -1000,10 +822,7 @@ export type CountResult< // #region Aggregate -export type AggregateArgs< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type AggregateArgs> = { where?: WhereInput; skip?: number; take?: number; @@ -1019,15 +838,8 @@ export type AggregateArgs< _max?: MinMaxInput; }); -type NumericFields< - Schema extends SchemaDef, - Model extends GetModels -> = keyof { - [Key in GetFields as GetFieldType< - Schema, - Model, - Key - > extends 'Int' | 'Float' | 'BigInt' | 'Decimal' +type NumericFields> = keyof { + [Key in GetFields as GetFieldType extends 'Int' | 'Float' | 'BigInt' | 'Decimal' ? FieldIsArray extends true ? never : Key @@ -1039,21 +851,17 @@ type SumAvgInput> = { }; type MinMaxInput> = { - [Key in GetFields as FieldIsArray< - Schema, - Model, - Key - > extends true + [Key in GetFields as FieldIsArray extends true ? never : FieldIsRelation extends true - ? never - : Key]?: true; + ? never + : Key]?: true; }; export type AggregateResult< Schema extends SchemaDef, Model extends GetModels, - Args extends AggregateArgs + Args extends AggregateArgs, > = (Args extends { _count: infer Count } ? { _count: AggCommonOutput; @@ -1083,24 +891,19 @@ export type AggregateResult< type AggCommonOutput = Input extends true ? number : Input extends {} - ? { - [Key in keyof Input]: number; - } - : never; + ? { + [Key in keyof Input]: number; + } + : never; // #endregion // #region GroupBy -export type GroupByArgs< - Schema extends SchemaDef, - Model extends GetModels -> = { +export type GroupByArgs> = { where?: WhereInput; orderBy?: OrArray>; - by: - | NonRelationFields - | NonEmptyArray>; + by: NonRelationFields | NonEmptyArray>; having?: WhereInput; take?: number; skip?: number; @@ -1117,13 +920,10 @@ export type GroupByArgs< export type GroupByResult< Schema extends SchemaDef, Model extends GetModels, - Args extends GroupByArgs + Args extends GroupByArgs, > = Array< { - [Key in NonRelationFields< - Schema, - Model - > as Key extends ValueOfPotentialTuple + [Key in NonRelationFields as Key extends ValueOfPotentialTuple ? Key : never]: MapFieldType; } & (Args extends { _count: infer Count } @@ -1160,88 +960,79 @@ export type GroupByResult< type ConnectInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = FieldIsArray extends true - ? OrArray>> - : WhereUniqueInput>; + Field extends RelationFields, +> = + FieldIsArray extends true + ? OrArray>> + : WhereUniqueInput>; type ConnectOrCreateInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = FieldIsArray extends true - ? OrArray< - ConnectOrCreatePayload< + Field extends RelationFields, +> = + FieldIsArray extends true + ? OrArray< + ConnectOrCreatePayload< + Schema, + RelationFieldType, + OppositeRelationAndFK + > + > + : ConnectOrCreatePayload< Schema, RelationFieldType, OppositeRelationAndFK - > - > - : ConnectOrCreatePayload< - Schema, - RelationFieldType, - OppositeRelationAndFK - >; + >; type DisconnectInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = FieldIsArray extends true - ? OrArray< - WhereUniqueInput>, - true - > - : boolean | WhereInput>; + Field extends RelationFields, +> = + FieldIsArray extends true + ? OrArray>, true> + : boolean | WhereInput>; type SetRelationInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = OrArray>>; type NestedUpdateInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = FieldIsArray extends true - ? OrArray< - { - where: WhereUniqueInput< - Schema, - RelationFieldType - >; - data: UpdateInput< - Schema, - RelationFieldType, - OppositeRelationAndFK - >; - }, - true - > - : XOR< - { - where: WhereUniqueInput< - Schema, - RelationFieldType - >; - data: UpdateInput< - Schema, - RelationFieldType, - OppositeRelationAndFK - >; - }, - UpdateInput< - Schema, - RelationFieldType, - OppositeRelationAndFK + Field extends RelationFields, +> = + FieldIsArray extends true + ? OrArray< + { + where: WhereUniqueInput>; + data: UpdateInput< + Schema, + RelationFieldType, + OppositeRelationAndFK + >; + }, + true > - >; + : XOR< + { + where: WhereUniqueInput>; + data: UpdateInput< + Schema, + RelationFieldType, + OppositeRelationAndFK + >; + }, + UpdateInput, OppositeRelationAndFK> + >; type NestedUpsertInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = OrArray< { where: WhereUniqueInput; @@ -1262,40 +1053,31 @@ type NestedUpsertInput< type NestedUpdateManyInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = OrArray< - UpdateManyPayload< - Schema, - RelationFieldType, - OppositeRelationAndFK - > + UpdateManyPayload, OppositeRelationAndFK> >; type NestedDeleteInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = FieldIsArray extends true - ? OrArray< - WhereUniqueInput>, - true - > - : boolean | WhereInput>; + Field extends RelationFields, +> = + FieldIsArray extends true + ? OrArray>, true> + : boolean | WhereInput>; type NestedDeleteManyInput< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields + Field extends RelationFields, > = OrArray, true>>; // #endregion //#region Client API -export interface ModelOperations< - Schema extends SchemaDef, - Model extends GetModels -> { +export interface ModelOperations> { /** * Returns a list of entities. * @param args - query args @@ -1378,7 +1160,7 @@ export interface ModelOperations< * ``` */ findMany>( - args?: SelectSubset> + args?: SelectSubset>, ): Promise[]>; /** @@ -1388,7 +1170,7 @@ export interface ModelOperations< * @see {@link findMany} */ findUnique>( - args?: SelectSubset> + args?: SelectSubset>, ): Promise | null>; /** @@ -1398,7 +1180,7 @@ export interface ModelOperations< * @see {@link findMany} */ findUniqueOrThrow>( - args?: SelectSubset> + args?: SelectSubset>, ): Promise>; /** @@ -1408,7 +1190,7 @@ export interface ModelOperations< * @see {@link findMany} */ findFirst>( - args?: SelectSubset> + args?: SelectSubset>, ): Promise | null>; /** @@ -1418,7 +1200,7 @@ export interface ModelOperations< * @see {@link findMany} */ findFirstOrThrow>( - args?: SelectSubset> + args?: SelectSubset>, ): Promise>; /** @@ -1474,7 +1256,7 @@ export interface ModelOperations< * ``` */ create>( - args: SelectSubset> + args: SelectSubset>, ): Promise>; /** @@ -1523,7 +1305,7 @@ export interface ModelOperations< * ``` */ createManyAndReturn>( - args?: SelectSubset> + args?: SelectSubset>, ): Promise[]>; /** @@ -1644,7 +1426,7 @@ export interface ModelOperations< * ``` */ update>( - args: SelectSubset> + args: SelectSubset>, ): Promise>; /** @@ -1668,7 +1450,7 @@ export interface ModelOperations< * }); */ updateMany>( - args: Subset> + args: Subset>, ): Promise; /** @@ -1694,7 +1476,7 @@ export interface ModelOperations< * ``` */ updateManyAndReturn>( - args: Subset> + args: Subset>, ): Promise[]>; /** @@ -1718,7 +1500,7 @@ export interface ModelOperations< * ``` */ upsert>( - args: SelectSubset> + args: SelectSubset>, ): Promise>; /** @@ -1741,7 +1523,7 @@ export interface ModelOperations< * ``` */ delete>( - args: SelectSubset> + args: SelectSubset>, ): Promise>; /** @@ -1764,7 +1546,7 @@ export interface ModelOperations< * ``` */ deleteMany>( - args?: Subset> + args?: Subset>, ): Promise; /** @@ -1786,7 +1568,7 @@ export interface ModelOperations< * }); // result: `{ _all: number, email: number }` */ count>( - args?: Subset> + args?: Subset>, ): Promise>; /** @@ -1807,7 +1589,7 @@ export interface ModelOperations< * }); // result: `{ _count: number, _avg: { age: number }, ... }` */ aggregate>( - args: Subset> + args: Subset>, ): Promise>; /** @@ -1843,7 +1625,7 @@ export interface ModelOperations< * }); */ groupBy>( - args: Subset> + args: Subset>, ): Promise>; } diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index 21451778..5205f01c 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -1,20 +1,8 @@ -import type { - Expression, - ExpressionBuilder, - ExpressionWrapper, - SqlBool, - ValueNode, -} from 'kysely'; +import type { Expression, ExpressionBuilder, ExpressionWrapper, SqlBool, ValueNode } from 'kysely'; import { sql, type SelectQueryBuilder } from 'kysely'; import invariant from 'tiny-invariant'; import { match, P } from 'ts-pattern'; -import type { - BuiltinType, - DataSourceProviderType, - FieldDef, - GetModels, - SchemaDef, -} from '../../../schema'; +import type { BuiltinType, DataSourceProviderType, FieldDef, GetModels, SchemaDef } from '../../../schema'; import { enumerate } from '../../../utils/enumerate'; // @ts-expect-error import { isPlainObject } from 'is-plain-object'; @@ -46,7 +34,7 @@ import { export abstract class BaseCrudDialect { constructor( protected readonly schema: Schema, - protected readonly options: ClientOptions + protected readonly options: ClientOptions, ) {} abstract get provider(): DataSourceProviderType; @@ -60,20 +48,20 @@ export abstract class BaseCrudDialect { model: string, relationField: string, parentAlias: string, - payload: true | FindArgs, true> + payload: true | FindArgs, true>, ): SelectQueryBuilder; abstract buildSkipTake( query: SelectQueryBuilder, skip: number | undefined, - take: number | undefined + take: number | undefined, ): SelectQueryBuilder; buildFilter( eb: ExpressionBuilder, model: string, modelAlias: string, - where: boolean | object | undefined + where: boolean | object | undefined, ) { if (where === true || where === undefined) { return this.true(eb); @@ -96,60 +84,17 @@ export abstract class BaseCrudDialect { } if (key === 'AND' || key === 'OR' || key === 'NOT') { - result = this.and( - eb, - result, - this.buildCompositeFilter( - eb, - model, - modelAlias, - key, - payload - ) - ); + result = this.and(eb, result, this.buildCompositeFilter(eb, model, modelAlias, key, payload)); continue; } const fieldDef = requireField(this.schema, model, key); if (fieldDef.relation) { - result = this.and( - eb, - result, - this.buildRelationFilter( - eb, - model, - modelAlias, - key, - fieldDef, - payload - ) - ); + result = this.and(eb, result, this.buildRelationFilter(eb, model, modelAlias, key, fieldDef, payload)); } else if (fieldDef.array) { - result = this.and( - eb, - result, - this.buildArrayFilter( - eb, - model, - modelAlias, - key, - fieldDef, - payload - ) - ); + result = this.and(eb, result, this.buildArrayFilter(eb, model, modelAlias, key, fieldDef, payload)); } else { - result = this.and( - eb, - result, - this.buildPrimitiveFilter( - eb, - model, - modelAlias, - key, - fieldDef, - payload - ) - ); + result = this.and(eb, result, this.buildPrimitiveFilter(eb, model, modelAlias, key, fieldDef, payload)); } } @@ -166,36 +111,22 @@ export abstract class BaseCrudDialect { model: string, modelAlias: string, key: 'AND' | 'OR' | 'NOT', - payload: any + payload: any, ): Expression { return match(key) .with('AND', () => this.and( eb, - ...enumerate(payload).map((subPayload) => - this.buildFilter(eb, model, modelAlias, subPayload) - ) - ) + ...enumerate(payload).map((subPayload) => this.buildFilter(eb, model, modelAlias, subPayload)), + ), ) .with('OR', () => this.or( eb, - ...enumerate(payload).map((subPayload) => - this.buildFilter(eb, model, modelAlias, subPayload) - ) - ) - ) - .with('NOT', () => - eb.not( - this.buildCompositeFilter( - eb, - model, - modelAlias, - 'AND', - payload - ) - ) + ...enumerate(payload).map((subPayload) => this.buildFilter(eb, model, modelAlias, subPayload)), + ), ) + .with('NOT', () => eb.not(this.buildCompositeFilter(eb, model, modelAlias, 'AND', payload))) .exhaustive(); } @@ -205,26 +136,12 @@ export abstract class BaseCrudDialect { modelAlias: string, field: string, fieldDef: FieldDef, - payload: any + payload: any, ) { if (!fieldDef.array) { - return this.buildToOneRelationFilter( - eb, - model, - modelAlias, - field, - fieldDef, - payload - ); + return this.buildToOneRelationFilter(eb, model, modelAlias, field, fieldDef, payload); } else { - return this.buildToManyRelationFilter( - eb, - model, - modelAlias, - field, - fieldDef, - payload - ); + return this.buildToManyRelationFilter(eb, model, modelAlias, field, fieldDef, payload); } } @@ -234,56 +151,27 @@ export abstract class BaseCrudDialect { table: string, field: string, fieldDef: FieldDef, - payload: any + payload: any, ): Expression { if (payload === null) { - const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( - this.schema, - model, - field - ); + const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs(this.schema, model, field); if (ownedByModel) { // can be short-circuited to FK null check - return this.and( - eb, - ...keyPairs.map(({ fk }) => - eb(sql.ref(`${table}.${fk}`), 'is', null) - ) - ); + return this.and(eb, ...keyPairs.map(({ fk }) => eb(sql.ref(`${table}.${fk}`), 'is', null))); } else { // translate it to `{ is: null }` filter - return this.buildToOneRelationFilter( - eb, - model, - table, - field, - fieldDef, - { is: null } - ); + return this.buildToOneRelationFilter(eb, model, table, field, fieldDef, { is: null }); } } const joinAlias = `${table}$${field}`; - const joinPairs = buildJoinPairs( - this.schema, - model, - table, - field, - joinAlias - ); + const joinPairs = buildJoinPairs(this.schema, model, table, field, joinAlias); const filterResultField = `${field}$filter`; const joinSelect = eb .selectFrom(`${fieldDef.type} as ${joinAlias}`) - .where(() => - this.and( - eb, - ...joinPairs.map(([left, right]) => - eb(sql.ref(left), '=', sql.ref(right)) - ) - ) - ) + .where(() => this.and(eb, ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right))))) .select(() => eb.fn.count(eb.lit(1)).as(filterResultField)); const conditions: Expression[] = []; @@ -297,17 +185,10 @@ export abstract class BaseCrudDialect { // check if found conditions.push( eb( - joinSelect.where(() => - this.buildFilter( - eb, - fieldDef.type, - joinAlias, - payload.is - ) - ), + joinSelect.where(() => this.buildFilter(eb, fieldDef.type, joinAlias, payload.is)), '>', - 0 - ) + 0, + ), ); } } @@ -324,30 +205,21 @@ export abstract class BaseCrudDialect { eb(joinSelect, '=', 0), // found one that matches the filter eb( - joinSelect.where(() => - this.buildFilter( - eb, - fieldDef.type, - joinAlias, - payload.isNot - ) - ), + joinSelect.where(() => this.buildFilter(eb, fieldDef.type, joinAlias, payload.isNot)), '=', - 0 - ) - ) + 0, + ), + ), ); } } } else { conditions.push( eb( - joinSelect.where(() => - this.buildFilter(eb, fieldDef.type, joinAlias, payload) - ), + joinSelect.where(() => this.buildFilter(eb, fieldDef.type, joinAlias, payload)), '>', - 0 - ) + 0, + ), ); } @@ -360,7 +232,7 @@ export abstract class BaseCrudDialect { table: string, field: string, fieldDef: FieldDef, - payload: any + payload: any, ) { // null check needs to be converted to fk "is null" checks if (payload === null) { @@ -374,10 +246,7 @@ export abstract class BaseCrudDialect { if (m2m) { // many-to-many relation const modelIdField = getIdFields(this.schema, model)[0]!; - const relationIdField = getIdFields( - this.schema, - relationModel - )[0]!; + const relationIdField = getIdFields(this.schema, relationModel)[0]!; return eb( sql.ref(`${relationModel}.${relationIdField}`), 'in', @@ -387,15 +256,11 @@ export abstract class BaseCrudDialect { .whereRef( sql.ref(`${m2m.joinTable}.${m2m.parentFkName}`), '=', - sql.ref(`${table}.${modelIdField}`) - ) + sql.ref(`${table}.${modelIdField}`), + ), ); } else { - const relationKeyPairs = getRelationForeignKeyFieldPairs( - this.schema, - model, - field - ); + const relationKeyPairs = getRelationForeignKeyFieldPairs(this.schema, model, field); let result = this.true(eb); for (const { fk, pk } of relationKeyPairs.keyPairs) { @@ -403,21 +268,13 @@ export abstract class BaseCrudDialect { result = this.and( eb, result, - eb( - sql.ref(`${table}.${fk}`), - '=', - sql.ref(`${relationModel}.${pk}`) - ) + eb(sql.ref(`${table}.${fk}`), '=', sql.ref(`${relationModel}.${pk}`)), ); } else { result = this.and( eb, result, - eb( - sql.ref(`${table}.${pk}`), - '=', - sql.ref(`${relationModel}.${fk}`) - ) + eb(sql.ref(`${table}.${pk}`), '=', sql.ref(`${relationModel}.${fk}`)), ); } } @@ -440,21 +297,12 @@ export abstract class BaseCrudDialect { eb( eb .selectFrom(relationModel) - .select((eb1) => - eb1.fn.count(eb1.lit(1)).as('count') - ) + .select((eb1) => eb1.fn.count(eb1.lit(1)).as('count')) .where(buildPkFkWhereRefs(eb)) - .where((eb1) => - this.buildFilter( - eb1, - relationModel, - relationModel, - subPayload - ) - ), + .where((eb1) => this.buildFilter(eb1, relationModel, relationModel, subPayload)), '>', - 0 - ) + 0, + ), ); break; } @@ -466,23 +314,14 @@ export abstract class BaseCrudDialect { eb( eb .selectFrom(relationModel) - .select((eb1) => - eb1.fn.count(eb1.lit(1)).as('count') - ) + .select((eb1) => eb1.fn.count(eb1.lit(1)).as('count')) .where(buildPkFkWhereRefs(eb)) .where((eb1) => - eb1.not( - this.buildFilter( - eb1, - relationModel, - relationModel, - subPayload - ) - ) + eb1.not(this.buildFilter(eb1, relationModel, relationModel, subPayload)), ), '=', - 0 - ) + 0, + ), ); break; } @@ -494,21 +333,12 @@ export abstract class BaseCrudDialect { eb( eb .selectFrom(relationModel) - .select((eb1) => - eb1.fn.count(eb1.lit(1)).as('count') - ) + .select((eb1) => eb1.fn.count(eb1.lit(1)).as('count')) .where(buildPkFkWhereRefs(eb)) - .where((eb1) => - this.buildFilter( - eb1, - relationModel, - relationModel, - subPayload - ) - ), + .where((eb1) => this.buildFilter(eb1, relationModel, relationModel, subPayload)), '=', - 0 - ) + 0, + ), ); break; } @@ -524,18 +354,11 @@ export abstract class BaseCrudDialect { modelAlias: string, field: string, fieldDef: FieldDef, - payload: any + payload: any, ) { const clauses: Expression[] = []; const fieldType = fieldDef.type as BuiltinType; - const fieldRef = buildFieldRef( - this.schema, - model, - field, - this.options, - eb, - modelAlias - ); + const fieldRef = buildFieldRef(this.schema, model, field, this.options, eb, modelAlias); for (const [key, _value] of Object.entries(payload)) { if (_value === undefined) { @@ -546,14 +369,7 @@ export abstract class BaseCrudDialect { switch (key) { case 'equals': { - clauses.push( - this.buildLiteralFilter( - eb, - fieldRef, - fieldType, - eb.val(value) - ) - ); + clauses.push(this.buildLiteralFilter(eb, fieldRef, fieldType, eb.val(value))); break; } @@ -573,9 +389,7 @@ export abstract class BaseCrudDialect { } case 'isEmpty': { - clauses.push( - eb(fieldRef, value === true ? '=' : '!=', eb.val([])) - ); + clauses.push(eb(fieldRef, value === true ? '=' : '!=', eb.val([]))); break; } @@ -594,61 +408,29 @@ export abstract class BaseCrudDialect { modelAlias: string, field: string, fieldDef: FieldDef, - payload: any + payload: any, ) { if (payload === null) { return eb(sql.ref(`${modelAlias}.${field}`), 'is', null); } if (isEnum(this.schema, fieldDef.type)) { - return this.buildEnumFilter( - eb, - modelAlias, - field, - fieldDef, - payload - ); + return this.buildEnumFilter(eb, modelAlias, field, fieldDef, payload); } return match(fieldDef.type as BuiltinType) - .with('String', () => - this.buildStringFilter(eb, modelAlias, field, payload) - ) + .with('String', () => this.buildStringFilter(eb, modelAlias, field, payload)) .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.buildNumberFilter( - eb, - model, - modelAlias, - field, - type, - payload - ) - ) - .with('Boolean', () => - this.buildBooleanFilter(eb, modelAlias, field, payload) - ) - .with('DateTime', () => - this.buildDateTimeFilter(eb, modelAlias, field, payload) - ) - .with('Bytes', () => - this.buildBytesFilter(eb, modelAlias, field, payload) + this.buildNumberFilter(eb, model, modelAlias, field, type, payload), ) + .with('Boolean', () => this.buildBooleanFilter(eb, modelAlias, field, payload)) + .with('DateTime', () => this.buildDateTimeFilter(eb, modelAlias, field, payload)) + .with('Bytes', () => this.buildBytesFilter(eb, modelAlias, field, payload)) .exhaustive(); } - private buildLiteralFilter( - eb: ExpressionBuilder, - lhs: Expression, - type: BuiltinType, - rhs: unknown - ) { - return eb( - lhs, - '=', - rhs !== null && rhs !== undefined - ? this.transformPrimitive(rhs, type) - : rhs - ); + private buildLiteralFilter(eb: ExpressionBuilder, lhs: Expression, type: BuiltinType, rhs: unknown) { + return eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformPrimitive(rhs, type) : rhs); } private buildStandardFilter( @@ -659,7 +441,7 @@ export abstract class BaseCrudDialect { getRhs: (value: unknown) => any, recurse: (value: unknown) => Expression, throwIfInvalid = false, - onlyForKeys: string[] | undefined = undefined + onlyForKeys: string[] | undefined = undefined, ) { if (payload === null || !isPlainObject(payload)) { return { @@ -675,18 +457,11 @@ export abstract class BaseCrudDialect { if (onlyForKeys && !onlyForKeys.includes(op)) { continue; } - const rhs = Array.isArray(value) - ? value.map(getRhs) - : getRhs(value); + const rhs = Array.isArray(value) ? value.map(getRhs) : getRhs(value); const condition = match(op) - .with('equals', () => - rhs === null ? eb(lhs, 'is', null) : eb(lhs, '=', rhs) - ) + .with('equals', () => (rhs === null ? eb(lhs, 'is', null) : eb(lhs, '=', rhs))) .with('in', () => { - invariant( - Array.isArray(rhs), - 'right hand side must be an array' - ); + invariant(Array.isArray(rhs), 'right hand side must be an array'); if (rhs.length === 0) { return this.false(eb); } else { @@ -694,10 +469,7 @@ export abstract class BaseCrudDialect { } }) .with('notIn', () => { - invariant( - Array.isArray(rhs), - 'right hand side must be an array' - ); + invariant(Array.isArray(rhs), 'right hand side must be an array'); if (rhs.length === 0) { return this.true(eb); } else { @@ -730,20 +502,13 @@ export abstract class BaseCrudDialect { eb: ExpressionBuilder, table: string, field: string, - payload: StringFilter + payload: StringFilter, ) { const fieldDef = getField(this.schema, table, field); - let fieldRef: Expression = fieldDef?.computed - ? sql.ref(field) - : sql.ref(`${table}.${field}`); + let fieldRef: Expression = fieldDef?.computed ? sql.ref(field) : sql.ref(`${table}.${field}`); let insensitive = false; - if ( - payload && - typeof payload === 'object' && - 'mode' in payload && - payload.mode === 'insensitive' - ) { + if (payload && typeof payload === 'object' && 'mode' in payload && payload.mode === 'insensitive') { insensitive = true; fieldRef = eb.fn('lower', [fieldRef]); } @@ -754,13 +519,7 @@ export abstract class BaseCrudDialect { payload, fieldRef, (value) => this.prepStringCasing(eb, value, insensitive), - (value) => - this.buildStringFilter( - eb, - table, - field, - value as StringFilter - ) + (value) => this.buildStringFilter(eb, table, field, value as StringFilter), ); if (payload && typeof payload === 'object') { @@ -774,17 +533,17 @@ export abstract class BaseCrudDialect { .with('contains', () => insensitive ? eb(fieldRef, 'ilike', sql.lit(`%${value}%`)) - : eb(fieldRef, 'like', sql.lit(`%${value}%`)) + : eb(fieldRef, 'like', sql.lit(`%${value}%`)), ) .with('startsWith', () => insensitive ? eb(fieldRef, 'ilike', sql.lit(`${value}%`)) - : eb(fieldRef, 'like', sql.lit(`${value}%`)) + : eb(fieldRef, 'like', sql.lit(`${value}%`)), ) .with('endsWith', () => insensitive ? eb(fieldRef, 'ilike', sql.lit(`%${value}`)) - : eb(fieldRef, 'like', sql.lit(`%${value}`)) + : eb(fieldRef, 'like', sql.lit(`%${value}`)), ) .otherwise(() => { throw new Error(`Invalid string filter key: ${key}`); @@ -799,11 +558,7 @@ export abstract class BaseCrudDialect { return this.and(eb, ...conditions); } - private prepStringCasing( - eb: ExpressionBuilder, - value: unknown, - toLower: boolean = true - ): any { + private prepStringCasing(eb: ExpressionBuilder, value: unknown, toLower: boolean = true): any { if (typeof value === 'string') { return toLower ? eb.fn('lower', [sql.lit(value)]) : sql.lit(value); } else if (Array.isArray(value)) { @@ -819,7 +574,7 @@ export abstract class BaseCrudDialect { table: string, field: string, type: BuiltinType, - payload: any + payload: any, ) { const { conditions } = this.buildStandardFilter( eb, @@ -827,8 +582,7 @@ export abstract class BaseCrudDialect { payload, buildFieldRef(this.schema, model, field, this.options, eb), (value) => this.transformPrimitive(value, type), - (value) => - this.buildNumberFilter(eb, model, table, field, type, value) + (value) => this.buildNumberFilter(eb, model, table, field, type, value), ); return this.and(eb, ...conditions); } @@ -837,7 +591,7 @@ export abstract class BaseCrudDialect { eb: ExpressionBuilder, table: string, field: string, - payload: BooleanFilter + payload: BooleanFilter, ) { const { conditions } = this.buildStandardFilter( eb, @@ -845,15 +599,9 @@ export abstract class BaseCrudDialect { payload, sql.ref(`${table}.${field}`), (value) => this.transformPrimitive(value, 'Boolean'), - (value) => - this.buildBooleanFilter( - eb, - table, - field, - value as BooleanFilter - ), + (value) => this.buildBooleanFilter(eb, table, field, value as BooleanFilter), true, - ['equals', 'not'] + ['equals', 'not'], ); return this.and(eb, ...conditions); } @@ -862,7 +610,7 @@ export abstract class BaseCrudDialect { eb: ExpressionBuilder, table: string, field: string, - payload: DateTimeFilter + payload: DateTimeFilter, ) { const { conditions } = this.buildStandardFilter( eb, @@ -870,14 +618,8 @@ export abstract class BaseCrudDialect { payload, sql.ref(`${table}.${field}`), (value) => this.transformPrimitive(value, 'DateTime'), - (value) => - this.buildDateTimeFilter( - eb, - table, - field, - value as DateTimeFilter - ), - true + (value) => this.buildDateTimeFilter(eb, table, field, value as DateTimeFilter), + true, ); return this.and(eb, ...conditions); } @@ -886,7 +628,7 @@ export abstract class BaseCrudDialect { eb: ExpressionBuilder, table: string, field: string, - payload: BytesFilter + payload: BytesFilter, ) { const conditions = this.buildStandardFilter( eb, @@ -894,15 +636,9 @@ export abstract class BaseCrudDialect { payload, sql.ref(`${table}.${field}`), (value) => this.transformPrimitive(value, 'Bytes'), - (value) => - this.buildBytesFilter( - eb, - table, - field, - value as BytesFilter - ), + (value) => this.buildBytesFilter(eb, table, field, value as BytesFilter), true, - ['equals', 'in', 'notIn', 'not'] + ['equals', 'in', 'notIn', 'not'], ); return this.and(eb, ...conditions.conditions); } @@ -912,7 +648,7 @@ export abstract class BaseCrudDialect { table: string, field: string, fieldDef: FieldDef, - payload: any + payload: any, ) { const conditions = this.buildStandardFilter( eb, @@ -922,7 +658,7 @@ export abstract class BaseCrudDialect { (value) => value, (value) => this.buildEnumFilter(eb, table, field, fieldDef, value), true, - ['equals', 'in', 'notIn', 'not'] + ['equals', 'in', 'notIn', 'not'], ); return this.and(eb, ...conditions.conditions); } @@ -931,11 +667,9 @@ export abstract class BaseCrudDialect { query: SelectQueryBuilder, model: string, modelAlias: string, - orderBy: - | OrArray, boolean, boolean>> - | undefined, + orderBy: OrArray, boolean, boolean>> | undefined, useDefaultIfEmpty: boolean, - negated: boolean + negated: boolean, ) { if (!orderBy) { if (useDefaultIfEmpty) { @@ -953,21 +687,13 @@ export abstract class BaseCrudDialect { } // aggregations - if ( - ['_count', '_avg', '_sum', '_min', '_max'].includes(field) - ) { - invariant( - value && typeof value === 'object', - `invalid orderBy value for field "${field}"` - ); + if (['_count', '_avg', '_sum', '_min', '_max'].includes(field)) { + invariant(value && typeof value === 'object', `invalid orderBy value for field "${field}"`); for (const [k, v] of Object.entries(value)) { - invariant( - v === 'asc' || v === 'desc', - `invalid orderBy value for field "${field}"` - ); + invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`); result = result.orderBy( (eb) => eb.fn(field.slice(1), [sql.ref(k)]), - sql.raw(this.negateSort(v, negated)) + sql.raw(this.negateSort(v, negated)), ); } continue; @@ -975,18 +701,12 @@ export abstract class BaseCrudDialect { switch (field) { case '_count': { - invariant( - value && typeof value === 'object', - 'invalid orderBy value for field "_count"' - ); + invariant(value && typeof value === 'object', 'invalid orderBy value for field "_count"'); for (const [k, v] of Object.entries(value)) { - invariant( - v === 'asc' || v === 'desc', - `invalid orderBy value for field "${field}"` - ); + invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`); result = result.orderBy( (eb) => eb.fn.count(sql.ref(k)), - sql.raw(this.negateSort(v, negated)) + sql.raw(this.negateSort(v, negated)), ); } continue; @@ -999,10 +719,7 @@ export abstract class BaseCrudDialect { if (!fieldDef.relation) { if (value === 'asc' || value === 'desc') { - result = result.orderBy( - sql.ref(`${modelAlias}.${field}`), - this.negateSort(value, negated) - ); + result = result.orderBy(sql.ref(`${modelAlias}.${field}`), this.negateSort(value, negated)); } else if ( value && typeof value === 'object' && @@ -1013,12 +730,7 @@ export abstract class BaseCrudDialect { ) { result = result.orderBy( sql.ref(`${modelAlias}.${field}`), - sql.raw( - `${this.negateSort( - value.sort, - negated - )} nulls ${value.nulls}` - ) + sql.raw(`${this.negateSort(value.sort, negated)} nulls ${value.nulls}`), ); } } else { @@ -1028,71 +740,39 @@ export abstract class BaseCrudDialect { if (fieldDef.array) { // order by to-many relation if (typeof value !== 'object') { - throw new QueryError( - `invalid orderBy value for field "${field}"` - ); + throw new QueryError(`invalid orderBy value for field "${field}"`); } if ('_count' in value) { invariant( - value._count === 'asc' || - value._count === 'desc', - 'invalid orderBy value for field "_count"' + value._count === 'asc' || value._count === 'desc', + 'invalid orderBy value for field "_count"', ); const sort = this.negateSort(value._count, negated); result = result.orderBy((eb) => { let subQuery = eb.selectFrom(relationModel); - const joinPairs = buildJoinPairs( - this.schema, - model, - modelAlias, - field, - relationModel - ); + const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, relationModel); subQuery = subQuery.where(() => this.and( eb, - ...joinPairs.map(([left, right]) => - eb( - sql.ref(left), - '=', - sql.ref(right) - ) - ) - ) - ); - subQuery = subQuery.select(() => - eb.fn.count(eb.lit(1)).as('_count') + ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right))), + ), ); + subQuery = subQuery.select(() => eb.fn.count(eb.lit(1)).as('_count')); return subQuery; }, sort); } } else { // order by to-one relation result = result.leftJoin(relationModel, (join) => { - const joinPairs = buildJoinPairs( - this.schema, - model, - modelAlias, - field, - relationModel - ); + const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, relationModel); return join.on((eb) => this.and( eb, - ...joinPairs.map(([left, right]) => - eb(sql.ref(left), '=', sql.ref(right)) - ) - ) + ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right))), + ), ); }); - result = this.buildOrderBy( - result, - fieldDef.type, - relationModel, - value, - false, - negated - ); + result = this.buildOrderBy(result, fieldDef.type, relationModel, value, false, negated); } } } @@ -1106,15 +786,11 @@ export abstract class BaseCrudDialect { } public true(eb: ExpressionBuilder): Expression { - return eb.lit( - this.transformPrimitive(true, 'Boolean') as boolean - ); + return eb.lit(this.transformPrimitive(true, 'Boolean') as boolean); } public false(eb: ExpressionBuilder): Expression { - return eb.lit( - this.transformPrimitive(false, 'Boolean') as boolean - ); + return eb.lit(this.transformPrimitive(false, 'Boolean') as boolean); } public isTrue(expression: Expression) { @@ -1122,10 +798,7 @@ export abstract class BaseCrudDialect { if (node.kind !== 'ValueNode') { return false; } - return ( - (node as ValueNode).value === true || - (node as ValueNode).value === 1 - ); + return (node as ValueNode).value === true || (node as ValueNode).value === 1; } public isFalse(expression: Expression) { @@ -1133,16 +806,10 @@ export abstract class BaseCrudDialect { if (node.kind !== 'ValueNode') { return false; } - return ( - (node as ValueNode).value === false || - (node as ValueNode).value === 0 - ); + return (node as ValueNode).value === false || (node as ValueNode).value === 0; } - protected and( - eb: ExpressionBuilder, - ...args: Expression[] - ) { + protected and(eb: ExpressionBuilder, ...args: Expression[]) { const nonTrueArgs = args.filter((arg) => !this.isTrue(arg)); if (nonTrueArgs.length === 0) { return this.true(eb); @@ -1153,10 +820,7 @@ export abstract class BaseCrudDialect { } } - protected or( - eb: ExpressionBuilder, - ...args: Expression[] - ) { + protected or(eb: ExpressionBuilder, ...args: Expression[]) { const nonFalseArgs = args.filter((arg) => !this.isFalse(arg)); if (nonFalseArgs.length === 0) { return this.false(eb); @@ -1167,10 +831,7 @@ export abstract class BaseCrudDialect { } } - protected not( - eb: ExpressionBuilder, - ...args: Expression[] - ) { + protected not(eb: ExpressionBuilder, ...args: Expression[]) { return eb.not(this.and(eb, ...args)); } @@ -1179,7 +840,7 @@ export abstract class BaseCrudDialect { */ abstract buildJsonObject( eb: ExpressionBuilder, - value: Record> + value: Record>, ): ExpressionWrapper; /** @@ -1187,7 +848,7 @@ export abstract class BaseCrudDialect { */ abstract buildArrayLength( eb: ExpressionBuilder, - array: Expression + array: Expression, ): ExpressionWrapper; /** diff --git a/packages/runtime/src/client/crud/dialects/index.ts b/packages/runtime/src/client/crud/dialects/index.ts index 857eb5ac..9d67009e 100644 --- a/packages/runtime/src/client/crud/dialects/index.ts +++ b/packages/runtime/src/client/crud/dialects/index.ts @@ -7,7 +7,7 @@ import { SqliteCrudDialect } from './sqlite'; export function getCrudDialect( schema: Schema, - options: ClientOptions + options: ClientOptions, ): BaseCrudDialect { return match(schema.provider.type) .with('sqlite', () => new SqliteCrudDialect(schema, options)) diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index 9714432f..f51e6bb1 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -8,12 +8,7 @@ import { } from 'kysely'; import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; -import type { - BuiltinType, - FieldDef, - GetModels, - SchemaDef, -} from '../../../schema'; +import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schema'; import type { FindArgs } from '../../crud-types'; import { buildFieldRef, @@ -25,9 +20,7 @@ import { } from '../../query-utils'; import { BaseCrudDialect } from './base'; -export class PostgresCrudDialect< - Schema extends SchemaDef -> extends BaseCrudDialect { +export class PostgresCrudDialect extends BaseCrudDialect { override get provider() { return 'postgresql' as const; } @@ -42,11 +35,7 @@ export class PostgresCrudDialect< } else { return match(type) .with('DateTime', () => - value instanceof Date - ? value - : typeof value === 'string' - ? new Date(value) - : value + value instanceof Date ? value : typeof value === 'string' ? new Date(value) : value, ) .otherwise(() => value); } @@ -57,19 +46,11 @@ export class PostgresCrudDialect< model: string, relationField: string, parentAlias: string, - payload: true | FindArgs, true> + payload: true | FindArgs, true>, ): SelectQueryBuilder { - const joinedQuery = this.buildRelationJSON( - model, - query, - relationField, - parentAlias, - payload - ); + const joinedQuery = this.buildRelationJSON(model, query, relationField, parentAlias, payload); - return joinedQuery.select( - `${parentAlias}$${relationField}.$j as ${relationField}` - ); + return joinedQuery.select(`${parentAlias}$${relationField}.$j as ${relationField}`); } private buildRelationJSON( @@ -77,13 +58,9 @@ export class PostgresCrudDialect< qb: SelectQueryBuilder, relationField: string, parentName: string, - payload: true | FindArgs, true> + payload: true | FindArgs, true>, ) { - const relationFieldDef = requireField( - this.schema, - model, - relationField - ); + const relationFieldDef = requireField(this.schema, model, relationField); const relationModel = relationFieldDef.type as GetModels; return qb.leftJoinLateral( @@ -91,26 +68,17 @@ export class PostgresCrudDialect< const joinTableName = `${parentName}$${relationField}`; // simple select by default - let result = eb.selectFrom( - `${relationModel} as ${joinTableName}` - ); + let result = eb.selectFrom(`${relationModel} as ${joinTableName}`); // however if there're filter/orderBy/take/skip, // we need to build a subquery to handle them before aggregation result = eb.selectFrom(() => { - let subQuery = eb - .selectFrom(`${relationModel}`) - .selectAll(); + let subQuery = eb.selectFrom(`${relationModel}`).selectAll(); if (payload && typeof payload === 'object') { if (payload.where) { subQuery = subQuery.where((eb) => - this.buildFilter( - eb, - relationModel, - relationModel, - payload.where - ) + this.buildFilter(eb, relationModel, relationModel, payload.where), ); } @@ -131,64 +99,38 @@ export class PostgresCrudDialect< relationModel, payload.orderBy, skip !== undefined || take !== undefined, - negateOrderBy + negateOrderBy, ); } // add join conditions - const m2m = getManyToManyRelation( - this.schema, - model, - relationField - ); + const m2m = getManyToManyRelation(this.schema, model, relationField); if (m2m) { // many-to-many relation const parentIds = getIdFields(this.schema, model); - const relationIds = getIdFields( - this.schema, - relationModel - ); - invariant( - parentIds.length === 1, - 'many-to-many relation must have exactly one id field' - ); - invariant( - relationIds.length === 1, - 'many-to-many relation must have exactly one id field' - ); + const relationIds = getIdFields(this.schema, relationModel); + invariant(parentIds.length === 1, 'many-to-many relation must have exactly one id field'); + invariant(relationIds.length === 1, 'many-to-many relation must have exactly one id field'); subQuery = subQuery.where( eb( eb.ref(`${relationModel}.${relationIds[0]}`), 'in', eb .selectFrom(m2m.joinTable) - .select( - `${m2m.joinTable}.${m2m.otherFkName}` - ) + .select(`${m2m.joinTable}.${m2m.otherFkName}`) .whereRef( `${parentName}.${parentIds[0]}`, '=', - `${m2m.joinTable}.${m2m.parentFkName}` - ) - ) + `${m2m.joinTable}.${m2m.parentFkName}`, + ), + ), ); } else { - const joinPairs = buildJoinPairs( - this.schema, - model, - parentName, - relationField, - relationModel - ); + const joinPairs = buildJoinPairs(this.schema, model, parentName, relationField, relationModel); subQuery = subQuery.where((eb) => - this.and( - eb, - ...joinPairs.map(([left, right]) => - eb(sql.ref(left), '=', sql.ref(right)) - ) - ) + this.and(eb, ...joinPairs.map(([left, right]) => eb(sql.ref(left), '=', sql.ref(right)))), ); } @@ -201,22 +143,16 @@ export class PostgresCrudDialect< relationFieldDef, result, payload, - parentName + parentName, ); // add nested joins for each relation - result = this.buildRelationJoins( - relationModel, - relationField, - result, - payload, - parentName - ); + result = this.buildRelationJoins(relationModel, relationField, result, payload, parentName); // alias the join table return result.as(joinTableName); }, - (join) => join.onTrue() + (join) => join.onTrue(), ); } @@ -226,25 +162,14 @@ export class PostgresCrudDialect< relationFieldDef: FieldDef, qb: SelectQueryBuilder, payload: true | FindArgs, true>, - parentName: string + parentName: string, ) { qb = qb.select((eb) => { - const objArgs = this.buildRelationObjectArgs( - relationModel, - relationField, - eb, - payload, - parentName - ); + const objArgs = this.buildRelationObjectArgs(relationModel, relationField, eb, payload, parentName); if (relationFieldDef.array) { return eb.fn - .coalesce( - sql`jsonb_agg(jsonb_build_object(${sql.join( - objArgs - )}))`, - sql`'[]'::jsonb` - ) + .coalesce(sql`jsonb_agg(jsonb_build_object(${sql.join(objArgs)}))`, sql`'[]'::jsonb`) .as('$j'); } else { return sql`jsonb_build_object(${sql.join(objArgs)})`.as('$j'); @@ -259,14 +184,11 @@ export class PostgresCrudDialect< relationField: string, eb: ExpressionBuilder, payload: true | FindArgs, true>, - parentName: string + parentName: string, ) { const relationModelDef = requireModel(this.schema, relationModel); const objArgs: Array< - | string - | ExpressionWrapper - | SelectQueryBuilder - | RawBuilder + string | ExpressionWrapper | SelectQueryBuilder | RawBuilder > = []; if (payload === true || !payload.select) { @@ -274,24 +196,12 @@ export class PostgresCrudDialect< objArgs.push( ...Object.entries(relationModelDef.fields) .filter(([, value]) => !value.relation) - .filter( - ([name]) => - !( - typeof payload === 'object' && - (payload.omit as any)?.[name] === true - ) - ) + .filter(([name]) => !(typeof payload === 'object' && (payload.omit as any)?.[name] === true)) .map(([field]) => [ sql.lit(field), - buildFieldRef( - this.schema, - relationModel, - field, - this.options, - eb - ), + buildFieldRef(this.schema, relationModel, field, this.options, eb), ]) - .flatMap((v) => v) + .flatMap((v) => v), ); } else if (payload.select) { // select specific fields @@ -300,32 +210,19 @@ export class PostgresCrudDialect< .filter(([, value]) => value) .map(([field]) => [ sql.lit(field), - buildFieldRef( - this.schema, - relationModel, - field, - this.options, - eb - ), + buildFieldRef(this.schema, relationModel, field, this.options, eb), ]) - .flatMap((v) => v) + .flatMap((v) => v), ); } - if ( - typeof payload === 'object' && - payload.include && - typeof payload.include === 'object' - ) { + if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') { // include relation fields objArgs.push( ...Object.entries(payload.include) .filter(([, value]) => value) - .map(([field]) => [ - sql.lit(field), - eb.ref(`${parentName}$${relationField}$${field}.$j`), - ]) - .flatMap((v) => v) + .map(([field]) => [sql.lit(field), eb.ref(`${parentName}$${relationField}$${field}.$j`)]) + .flatMap((v) => v), ); } return objArgs; @@ -336,24 +233,14 @@ export class PostgresCrudDialect< relationField: string, qb: SelectQueryBuilder, payload: true | FindArgs, true>, - parentName: string + parentName: string, ) { let result = qb; - if ( - typeof payload === 'object' && - payload.include && - typeof payload.include === 'object' - ) { + if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') { Object.entries(payload.include) .filter(([, value]) => value) .forEach(([field, value]) => { - result = this.buildRelationJSON( - model, - result, - field, - `${parentName}$${relationField}`, - value - ); + result = this.buildRelationJSON(model, result, field, `${parentName}$${relationField}`, value); }); } return result; @@ -362,7 +249,7 @@ export class PostgresCrudDialect< override buildSkipTake( query: SelectQueryBuilder, skip: number | undefined, - take: number | undefined + take: number | undefined, ) { if (take !== undefined) { query = query.limit(take); @@ -373,16 +260,10 @@ export class PostgresCrudDialect< return query; } - override buildJsonObject( - eb: ExpressionBuilder, - value: Record> - ) { + override buildJsonObject(eb: ExpressionBuilder, value: Record>) { return eb.fn( 'jsonb_build_object', - Object.entries(value).flatMap(([key, value]) => [ - sql.lit(key), - value, - ]) + Object.entries(value).flatMap(([key, value]) => [sql.lit(key), value]), ); } @@ -400,7 +281,7 @@ export class PostgresCrudDialect< override buildArrayLength( eb: ExpressionBuilder, - array: Expression + array: Expression, ): ExpressionWrapper { return eb.fn('array_length', [array]); } @@ -409,9 +290,7 @@ export class PostgresCrudDialect< if (values.length === 0) { return '{}'; } else { - return `ARRAY[${values.map((v) => - typeof v === 'string' ? `'${v}'` : v - )}]`; + return `ARRAY[${values.map((v) => (typeof v === 'string' ? `'${v}'` : v))}]`; } } } diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index f1031935..2144317a 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -21,9 +21,7 @@ import { } from '../../query-utils'; import { BaseCrudDialect } from './base'; -export class SqliteCrudDialect< - Schema extends SchemaDef -> extends BaseCrudDialect { +export class SqliteCrudDialect extends BaseCrudDialect { override get provider() { return 'sqlite' as const; } @@ -38,9 +36,7 @@ export class SqliteCrudDialect< } else { return match(type) .with('Boolean', () => (value ? 1 : 0)) - .with('DateTime', () => - value instanceof Date ? value.toISOString() : value - ) + .with('DateTime', () => (value instanceof Date ? value.toISOString() : value)) .with('Decimal', () => (value as Decimal).toString()) .with('Bytes', () => Buffer.from(value as Uint8Array)) .otherwise(() => value); @@ -52,16 +48,10 @@ export class SqliteCrudDialect< model: string, relationField: string, parentAlias: string, - payload: true | FindArgs, true> + payload: true | FindArgs, true>, ): SelectQueryBuilder { return query.select((eb) => - this.buildRelationJSON( - model, - eb, - relationField, - parentAlias, - payload - ).as(relationField) + this.buildRelationJSON(model, eb, relationField, parentAlias, payload).as(relationField), ); } @@ -70,13 +60,9 @@ export class SqliteCrudDialect< eb: ExpressionBuilder, relationField: string, parentName: string, - payload: true | FindArgs, true> + payload: true | FindArgs, true>, ) { - const relationFieldDef = requireField( - this.schema, - model, - relationField - ); + const relationFieldDef = requireField(this.schema, model, relationField); const relationModel = relationFieldDef.type as GetModels; const relationModelDef = requireModel(this.schema, relationModel); @@ -88,12 +74,7 @@ export class SqliteCrudDialect< if (payload && typeof payload === 'object') { if (payload.where) { subQuery = subQuery.where((eb) => - this.buildFilter( - eb, - relationModel, - relationModel, - payload.where - ) + this.buildFilter(eb, relationModel, relationModel, payload.where), ); } @@ -114,29 +95,19 @@ export class SqliteCrudDialect< relationModel, payload.orderBy, skip !== undefined || take !== undefined, - negateOrderBy + negateOrderBy, ); } // join conditions - const m2m = getManyToManyRelation( - this.schema, - model, - relationField - ); + const m2m = getManyToManyRelation(this.schema, model, relationField); if (m2m) { // many-to-many relation const parentIds = getIdFields(this.schema, model); const relationIds = getIdFields(this.schema, relationModel); - invariant( - parentIds.length === 1, - 'many-to-many relation must have exactly one id field' - ); - invariant( - relationIds.length === 1, - 'many-to-many relation must have exactly one id field' - ); + invariant(parentIds.length === 1, 'many-to-many relation must have exactly one id field'); + invariant(relationIds.length === 1, 'many-to-many relation must have exactly one id field'); subQuery = subQuery.where( eb( eb.ref(`${relationModel}.${relationIds[0]}`), @@ -144,35 +115,18 @@ export class SqliteCrudDialect< eb .selectFrom(m2m.joinTable) .select(`${m2m.joinTable}.${m2m.otherFkName}`) - .whereRef( - `${parentName}.${parentIds[0]}`, - '=', - `${m2m.joinTable}.${m2m.parentFkName}` - ) - ) + .whereRef(`${parentName}.${parentIds[0]}`, '=', `${m2m.joinTable}.${m2m.parentFkName}`), + ), ); } else { - const { keyPairs, ownedByModel } = - getRelationForeignKeyFieldPairs( - this.schema, - model, - relationField - ); + const { keyPairs, ownedByModel } = getRelationForeignKeyFieldPairs(this.schema, model, relationField); keyPairs.forEach(({ fk, pk }) => { if (ownedByModel) { // the parent model owns the fk - subQuery = subQuery.whereRef( - `${relationModel}.${pk}`, - '=', - `${parentName}.${fk}` - ); + subQuery = subQuery.whereRef(`${relationModel}.${pk}`, '=', `${parentName}.${fk}`); } else { // the relation side owns the fk - subQuery = subQuery.whereRef( - `${relationModel}.${fk}`, - '=', - `${parentName}.${pk}` - ); + subQuery = subQuery.whereRef(`${relationModel}.${fk}`, '=', `${parentName}.${pk}`); } }); } @@ -180,10 +134,7 @@ export class SqliteCrudDialect< }); tbl = tbl.select(() => { - type ArgsType = - | Expression - | RawBuilder - | SelectQueryBuilder; + type ArgsType = Expression | RawBuilder | SelectQueryBuilder; const objArgs: ArgsType[] = []; if (payload === true || !payload.select) { @@ -191,24 +142,12 @@ export class SqliteCrudDialect< objArgs.push( ...Object.entries(relationModelDef.fields) .filter(([, value]) => !value.relation) - .filter( - ([name]) => - !( - typeof payload === 'object' && - (payload.omit as any)?.[name] === true - ) - ) + .filter(([name]) => !(typeof payload === 'object' && (payload.omit as any)?.[name] === true)) .map(([field]) => [ sql.lit(field), - buildFieldRef( - this.schema, - relationModel, - field, - this.options, - eb - ), + buildFieldRef(this.schema, relationModel, field, this.options, eb), ]) - .flatMap((v) => v) + .flatMap((v) => v), ); } else if (payload.select) { // select specific fields @@ -216,42 +155,28 @@ export class SqliteCrudDialect< ...Object.entries(payload.select) .filter(([, value]) => value) .map(([field, value]) => { - const fieldDef = requireField( - this.schema, - relationModel, - field - ); + const fieldDef = requireField(this.schema, relationModel, field); if (fieldDef.relation) { const subJson = this.buildRelationJSON( relationModel as GetModels, eb, field, `${parentName}$${relationField}`, - value + value, ); return [sql.lit(field), subJson as ArgsType]; } else { return [ sql.lit(field), - buildFieldRef( - this.schema, - relationModel, - field, - this.options, - eb - ) as ArgsType, + buildFieldRef(this.schema, relationModel, field, this.options, eb) as ArgsType, ]; } }) - .flatMap((v) => v) + .flatMap((v) => v), ); } - if ( - typeof payload === 'object' && - payload.include && - typeof payload.include === 'object' - ) { + if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') { // include relation fields objArgs.push( ...Object.entries(payload.include) @@ -262,22 +187,17 @@ export class SqliteCrudDialect< eb, field, `${parentName}$${relationField}`, - value + value, ); return [sql.lit(field), subJson]; }) - .flatMap((v) => v) + .flatMap((v) => v), ); } if (relationFieldDef.array) { return eb.fn - .coalesce( - sql`json_group_array(json_object(${sql.join( - objArgs - )}))`, - sql`json_array()` - ) + .coalesce(sql`json_group_array(json_object(${sql.join(objArgs)}))`, sql`json_array()`) .as('$j'); } else { return sql`json_object(${sql.join(objArgs)})`.as('data'); @@ -290,7 +210,7 @@ export class SqliteCrudDialect< override buildSkipTake( query: SelectQueryBuilder, skip: number | undefined, - take: number | undefined + take: number | undefined, ) { if (take !== undefined) { query = query.limit(take); @@ -305,16 +225,10 @@ export class SqliteCrudDialect< return query; } - override buildJsonObject( - eb: ExpressionBuilder, - value: Record> - ) { + override buildJsonObject(eb: ExpressionBuilder, value: Record>) { return eb.fn( 'json_object', - Object.entries(value).flatMap(([key, value]) => [ - sql.lit(key), - value, - ]) + Object.entries(value).flatMap(([key, value]) => [sql.lit(key), value]), ); } @@ -332,7 +246,7 @@ export class SqliteCrudDialect< override buildArrayLength( eb: ExpressionBuilder, - array: Expression + array: Expression, ): ExpressionWrapper { return eb.fn('json_array_length', [array]); } diff --git a/packages/runtime/src/client/crud/operations/aggregate.ts b/packages/runtime/src/client/crud/operations/aggregate.ts index 65b98556..05392250 100644 --- a/packages/runtime/src/client/crud/operations/aggregate.ts +++ b/packages/runtime/src/client/crud/operations/aggregate.ts @@ -4,14 +4,9 @@ import type { SchemaDef } from '../../../schema'; import { getField } from '../../query-utils'; import { BaseOperationHandler } from './base'; -export class AggregateOperationHandler< - Schema extends SchemaDef -> extends BaseOperationHandler { +export class AggregateOperationHandler extends BaseOperationHandler { async handle(_operation: 'aggregate', args: unknown | undefined) { - const validatedArgs = this.inputValidator.validateAggregateArgs( - this.model, - args - ); + const validatedArgs = this.inputValidator.validateAggregateArgs(this.model, args); let query = this.kysely.selectFrom((eb) => { // nested query for filtering and pagination @@ -20,14 +15,7 @@ export class AggregateOperationHandler< let subQuery = eb .selectFrom(this.model) .selectAll(this.model as any) // TODO: check typing - .where((eb1) => - this.dialect.buildFilter( - eb1, - this.model, - this.model, - validatedArgs?.where - ) - ); + .where((eb1) => this.dialect.buildFilter(eb1, this.model, this.model, validatedArgs?.where)); // skip & take const skip = validatedArgs?.skip; @@ -46,7 +34,7 @@ export class AggregateOperationHandler< this.model, validatedArgs.orderBy, skip !== undefined || take !== undefined, - negateOrderBy + negateOrderBy, ); return subQuery.as('$sub'); @@ -57,28 +45,17 @@ export class AggregateOperationHandler< switch (key) { case '_count': { if (value === true) { - query = query.select((eb) => - eb.cast(eb.fn.countAll(), 'integer').as('_count') - ); + query = query.select((eb) => eb.cast(eb.fn.countAll(), 'integer').as('_count')); } else { Object.entries(value).forEach(([field, val]) => { if (val === true) { if (field === '_all') { query = query.select((eb) => - eb - .cast(eb.fn.countAll(), 'integer') - .as(`_count._all`) + eb.cast(eb.fn.countAll(), 'integer').as(`_count._all`), ); } else { query = query.select((eb) => - eb - .cast( - eb.fn.count( - sql.ref(`$sub.${field}`) - ), - 'integer' - ) - .as(`${key}.${field}`) + eb.cast(eb.fn.count(sql.ref(`$sub.${field}`)), 'integer').as(`${key}.${field}`), ); } } @@ -100,9 +77,7 @@ export class AggregateOperationHandler< .with('_max', () => eb.fn.max) .with('_min', () => eb.fn.min) .exhaustive(); - return fn(sql.ref(`$sub.${field}`)).as( - `${key}.${field}` - ); + return fn(sql.ref(`$sub.${field}`)).as(`${key}.${field}`); }); } }); diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index ce5f8ad8..dbecfe8a 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -17,26 +17,13 @@ import * as uuid from 'uuid'; import type { ClientContract } from '../..'; import { PolicyPlugin } from '../../../plugins/policy'; import type { BuiltinType, Expression, FieldDef } from '../../../schema'; -import { - ExpressionUtils, - type GetModels, - type ModelDef, - type SchemaDef, -} from '../../../schema'; +import { ExpressionUtils, type GetModels, type ModelDef, type SchemaDef } from '../../../schema'; import { clone } from '../../../utils/clone'; import { enumerate } from '../../../utils/enumerate'; -import { - extractFields, - fieldsToSelectObject, -} from '../../../utils/object-utils'; +import { extractFields, fieldsToSelectObject } from '../../../utils/object-utils'; import { CONTEXT_COMMENT_PREFIX, NUMERIC_FIELD_TYPES } from '../../constants'; import type { CRUD } from '../../contract'; -import type { - FindArgs, - SelectIncludeOmit, - SortOrder, - WhereInput, -} from '../../crud-types'; +import type { FindArgs, SelectIncludeOmit, SortOrder, WhereInput } from '../../crud-types'; import { InternalError, NotFoundError, QueryError } from '../../errors'; import type { ToKysely } from '../../query-builder'; import { @@ -91,7 +78,7 @@ export abstract class BaseOperationHandler { constructor( protected readonly client: ClientContract, protected readonly model: GetModels, - protected readonly inputValidator: InputValidator + protected readonly inputValidator: InputValidator, ) { this.dialect = getCrudDialect(this.schema, this.client.$options); } @@ -111,18 +98,12 @@ export abstract class BaseOperationHandler { abstract handle(operation: CrudOperation, args: any): Promise; withClient(client: ClientContract) { - return new (this.constructor as new (...args: any[]) => this)( - client, - this.model, - this.inputValidator - ); + return new (this.constructor as new (...args: any[]) => this)(client, this.model, this.inputValidator); } // TODO: this is not clean, needs a better solution protected get hasPolicyEnabled() { - return this.options.plugins?.some( - (plugin) => plugin instanceof PolicyPlugin - ); + return this.options.plugins?.some((plugin) => plugin instanceof PolicyPlugin); } protected requireModel(model: string) { @@ -141,17 +122,9 @@ export abstract class BaseOperationHandler { return getField(this.schema, model, field); } - protected exists( - kysely: ToKysely, - model: GetModels, - filter: any - ): Promise { + protected exists(kysely: ToKysely, model: GetModels, filter: any): Promise { const idFields = getIdFields(this.schema, model); - const _filter = flattenCompoundUniqueFilters( - this.schema, - model, - filter - ); + const _filter = flattenCompoundUniqueFilters(this.schema, model, filter); const query = kysely .selectFrom(model) .where((eb) => eb.and(_filter)) @@ -164,16 +137,14 @@ export abstract class BaseOperationHandler { protected async read( kysely: ToKysely, model: GetModels, - args: FindArgs, true> | undefined + args: FindArgs, true> | undefined, ): Promise { // table let query = kysely.selectFrom(model); // where if (args?.where) { - query = query.where((eb) => - this.dialect.buildFilter(eb, model, model, args?.where) - ); + query = query.where((eb) => this.dialect.buildFilter(eb, model, model, args?.where)); } // skip && take @@ -193,7 +164,7 @@ export abstract class BaseOperationHandler { model, args?.orderBy, skip !== undefined || take !== undefined, - negateOrderBy + negateOrderBy, ); // distinct @@ -201,9 +172,7 @@ export abstract class BaseOperationHandler { if (args?.distinct) { const distinct = ensureArray(args.distinct); if (this.dialect.supportsDistinctOn) { - query = query.distinctOn( - distinct.map((f: any) => sql.ref(`${model}.${f}`)) - ); + query = query.distinctOn(distinct.map((f: any) => sql.ref(`${model}.${f}`))); } else { // in-memory distinct after fetching all results inMemoryDistinct = distinct; @@ -221,45 +190,28 @@ export abstract class BaseOperationHandler { // include if (args?.include) { - query = this.buildFieldSelection( - model, - query, - args?.include, - model - ); + query = this.buildFieldSelection(model, query, args?.include, model); } if (args?.cursor) { - query = this.buildCursorFilter( - model, - query, - args.cursor, - args.orderBy, - negateOrderBy - ); + query = this.buildCursorFilter(model, query, args.cursor, args.orderBy, negateOrderBy); } - query = query.modifyEnd( - this.makeContextComment({ model, operation: 'read' }) - ); + query = query.modifyEnd(this.makeContextComment({ model, operation: 'read' })); let result: any[] = []; try { result = await query.execute(); } catch (err) { const { sql, parameters } = query.compile(); - throw new QueryError( - `Failed to execute query: ${err}, sql: ${sql}, parameters: ${parameters}` - ); + throw new QueryError(`Failed to execute query: ${err}, sql: ${sql}, parameters: ${parameters}`); } if (inMemoryDistinct) { const distinctResult: Record[] = []; const seen = new Set(); for (const r of result as any[]) { - const key = safeJSONStringify( - inMemoryDistinct.map((f) => r[f]) - )!; + const key = safeJSONStringify(inMemoryDistinct.map((f) => r[f]))!; if (!seen.has(key)) { distinctResult.push(r); seen.add(key); @@ -274,7 +226,7 @@ export abstract class BaseOperationHandler { protected async readUnique( kysely: ToKysely, model: GetModels, - args: FindArgs, true> + args: FindArgs, true>, ) { const result = await this.read(kysely, model, { ...args, take: 1 }); return result[0] ?? null; @@ -284,7 +236,7 @@ export abstract class BaseOperationHandler { model: string, query: SelectQueryBuilder, selectOrInclude: Record, - parentAlias: string + parentAlias: string, ) { let result = query; @@ -294,12 +246,7 @@ export abstract class BaseOperationHandler { } if (field === '_count') { - result = this.buildCountSelection( - result, - model, - parentAlias, - payload - ); + result = this.buildCountSelection(result, model, parentAlias, payload); continue; } @@ -308,17 +255,9 @@ export abstract class BaseOperationHandler { result = this.selectField(result, model, parentAlias, field); } else { if (!fieldDef.array && !fieldDef.optional && payload.where) { - throw new QueryError( - `Field "${field}" doesn't support filtering` - ); + throw new QueryError(`Field "${field}" doesn't support filtering`); } - result = this.dialect.buildRelationSelection( - result, - model, - field, - parentAlias, - payload - ); + result = this.dialect.buildRelationSelection(result, model, field, parentAlias, payload); } } @@ -329,20 +268,21 @@ export abstract class BaseOperationHandler { query: SelectQueryBuilder, model: string, parentAlias: string, - payload: any + payload: any, ) { const modelDef = requireModel(this.schema, model); - const toManyRelations = Object.entries(modelDef.fields).filter( - ([, field]) => field.relation && field.array - ); + const toManyRelations = Object.entries(modelDef.fields).filter(([, field]) => field.relation && field.array); const selections = payload === true ? { - select: toManyRelations.reduce((acc, [field]) => { - acc[field] = true; - return acc; - }, {} as Record), + select: toManyRelations.reduce( + (acc, [field]) => { + acc[field] = true; + return acc; + }, + {} as Record, + ), } : payload; @@ -353,13 +293,7 @@ export abstract class BaseOperationHandler { const fieldDef = requireField(this.schema, model, field); const fieldModel = fieldDef.type; const jointTable = `${parentAlias}$${field}$count`; - const joinPairs = buildJoinPairs( - this.schema, - model, - parentAlias, - field, - jointTable - ); + const joinPairs = buildJoinPairs(this.schema, model, parentAlias, field, jointTable); query = query.leftJoin( (eb) => { @@ -371,12 +305,7 @@ export abstract class BaseOperationHandler { value.where && typeof value.where === 'object' ) { - const filter = this.dialect.buildFilter( - eb, - fieldModel, - fieldModel, - value.where - ); + const filter = this.dialect.buildFilter(eb, fieldModel, fieldModel, value.where); result = result.where(filter); } return result.as(jointTable); @@ -386,38 +315,26 @@ export abstract class BaseOperationHandler { join = join.onRef(left, '=', right); } return join; - } + }, ); - jsonObject[field] = this.countIdDistinct( - eb, - fieldDef.type, - jointTable - ); + jsonObject[field] = this.countIdDistinct(eb, fieldDef.type, jointTable); } - query = query.select((eb) => - this.dialect.buildJsonObject(eb, jsonObject).as('_count') - ); + query = query.select((eb) => this.dialect.buildJsonObject(eb, jsonObject).as('_count')); return query; } - private countIdDistinct( - eb: ExpressionBuilder, - model: string, - table: string - ) { + private countIdDistinct(eb: ExpressionBuilder, model: string, table: string) { const idFields = getIdFields(this.schema, model); - return eb.fn - .count(sql.join(idFields.map((f) => sql.ref(`${table}.${f}`)))) - .distinct(); + return eb.fn.count(sql.join(idFields.map((f) => sql.ref(`${table}.${f}`)))).distinct(); } private buildSelectAllScalarFields( model: string, query: SelectQueryBuilder, - omit?: Record + omit?: Record, ) { const modelDef = this.requireModel(model); return Object.keys(modelDef.fields) @@ -426,21 +343,12 @@ export abstract class BaseOperationHandler { .reduce((acc, f) => this.selectField(acc, model, model, f), query); } - private selectField( - query: SelectQueryBuilder, - model: string, - modelAlias: string, - field: string - ) { + private selectField(query: SelectQueryBuilder, model: string, modelAlias: string, field: string) { const fieldDef = this.requireField(model, field); if (!fieldDef.computed) { return query.select(sql.ref(`${modelAlias}.${field}`).as(field)); } else { - return query.select((eb) => - buildFieldRef(this.schema, model, field, this.options, eb).as( - field - ) - ); + return query.select((eb) => buildFieldRef(this.schema, model, field, this.options, eb).as(field)); } } @@ -449,15 +357,13 @@ export abstract class BaseOperationHandler { query: SelectQueryBuilder, cursor: FindArgs, true>['cursor'], orderBy: FindArgs, true>['orderBy'], - negateOrderBy: boolean + negateOrderBy: boolean, ) { if (!orderBy) { orderBy = makeDefaultOrderBy(this.schema, model); } - const orderByItems = ensureArray(orderBy).flatMap((obj) => - Object.entries(obj) - ); + const orderByItems = ensureArray(orderBy).flatMap((obj) => Object.entries(obj)); const eb = expressionBuilder(); const cursorFilter = this.dialect.buildFilter(eb, model, model, cursor); @@ -470,21 +376,14 @@ export abstract class BaseOperationHandler { for (let j = 0; j <= i; j++) { const [field, order] = orderByItems[j]!; - const _order = negateOrderBy - ? order === 'asc' - ? 'desc' - : 'asc' - : order; + const _order = negateOrderBy ? (order === 'asc' ? 'desc' : 'asc') : order; const op = j === i ? (_order === 'asc' ? '>=' : '<=') : '='; andFilters.push( eb( eb.ref(`${model}.${field}`), op, - eb - .selectFrom(model) - .select(`${model}.${field}`) - .where(cursorFilter) - ) + eb.selectFrom(model).select(`${model}.${field}`).where(cursorFilter), + ), ); } @@ -500,36 +399,30 @@ export abstract class BaseOperationHandler { kysely: ToKysely, model: GetModels, data: any, - fromRelation?: FromRelationContext + fromRelation?: FromRelationContext, ): Promise { const modelDef = this.requireModel(model); const createFields: any = {}; - let parentUpdateTask: ((entity: any) => Promise) | undefined = - undefined; + let parentUpdateTask: ((entity: any) => Promise) | undefined = undefined; let m2m: ReturnType = undefined; if (fromRelation) { - m2m = getManyToManyRelation( - this.schema, - fromRelation.model, - fromRelation.field - ); + m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); if (!m2m) { // many-to-many relations are handled after create - const { ownedByModel, keyPairs } = - getRelationForeignKeyFieldPairs( - this.schema, - fromRelation?.model ?? '', - fromRelation?.field ?? '' - ); + const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( + this.schema, + fromRelation?.model ?? '', + fromRelation?.field ?? '', + ); if (!ownedByModel) { // assign fks from parent const parentFkFields = this.buildFkAssignments( fromRelation.model, fromRelation.field, - fromRelation.ids + fromRelation.ids, ); Object.assign(createFields, parentFkFields); } else { @@ -542,15 +435,15 @@ export abstract class BaseOperationHandler { ...acc, [fk]: entity[pk], }), - {} as any - ) + {} as any, + ), ) .where((eb) => eb.and(fromRelation.ids)) .modifyEnd( this.makeContextComment({ model: fromRelation.model, operation: 'update', - }) + }), ); return query.execute(); }; @@ -562,10 +455,7 @@ export abstract class BaseOperationHandler { const postCreateRelations: Record = {}; for (const [field, value] of Object.entries(data)) { const fieldDef = this.requireField(model, field); - if ( - isScalarField(this.schema, model, field) || - isForeignKeyField(this.schema, model, field) - ) { + if (isScalarField(this.schema, model, field) || isForeignKeyField(this.schema, model, field)) { if ( fieldDef.array && value && @@ -574,31 +464,16 @@ export abstract class BaseOperationHandler { Array.isArray(value.set) ) { // deal with nested "set" for scalar lists - createFields[field] = this.dialect.transformPrimitive( - value.set, - fieldDef.type as BuiltinType - ); + createFields[field] = this.dialect.transformPrimitive(value.set, fieldDef.type as BuiltinType); } else { - createFields[field] = this.dialect.transformPrimitive( - value, - fieldDef.type as BuiltinType - ); + createFields[field] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType); } } else { const subM2M = getManyToManyRelation(this.schema, model, field); - if ( - !subM2M && - fieldDef.relation?.fields && - fieldDef.relation?.references - ) { - const fkValues = await this.processOwnedRelation( - kysely, - fieldDef, - value - ); + if (!subM2M && fieldDef.relation?.fields && fieldDef.relation?.references) { + const fkValues = await this.processOwnedRelation(kysely, fieldDef, value); for (let i = 0; i < fieldDef.relation.fields.length; i++) { - createFields[fieldDef.relation.fields[i]!] = - fkValues[fieldDef.relation.references[i]!]; + createFields[fieldDef.relation.fields[i]!] = fkValues[fieldDef.relation.references[i]!]; } } else { const subPayload = value; @@ -619,7 +494,7 @@ export abstract class BaseOperationHandler { this.makeContextComment({ model, operation: 'create', - }) + }), ); const createdEntity = await query.executeTakeFirst(); @@ -635,17 +510,9 @@ export abstract class BaseOperationHandler { if (Object.keys(postCreateRelations).length > 0) { // process nested creates that need to happen after the current entity is created - const relationPromises = Object.entries(postCreateRelations).map( - ([field, subPayload]) => { - return this.processNoneOwnedRelation( - kysely, - model, - field, - subPayload, - createdEntity - ); - } - ); + const relationPromises = Object.entries(postCreateRelations).map(([field, subPayload]) => { + return this.processNoneOwnedRelation(kysely, model, field, subPayload, createdEntity); + }); // await relation creation await Promise.all(relationPromises); @@ -662,7 +529,7 @@ export abstract class BaseOperationHandler { m2m.otherModel, m2m.otherField, createdEntity, - m2m.joinTable + m2m.joinTable, ); } @@ -674,33 +541,17 @@ export abstract class BaseOperationHandler { return createdEntity; } - private buildFkAssignments( - model: string, - relationField: string, - entity: any - ) { + private buildFkAssignments(model: string, relationField: string, entity: any) { const parentFkFields: any = {}; - invariant( - relationField, - 'parentField must be defined if parentModel is defined' - ); - invariant( - entity, - 'parentEntity must be defined if parentModel is defined' - ); + invariant(relationField, 'parentField must be defined if parentModel is defined'); + invariant(entity, 'parentEntity must be defined if parentModel is defined'); - const { keyPairs } = getRelationForeignKeyFieldPairs( - this.schema, - model, - relationField - ); + const { keyPairs } = getRelationForeignKeyFieldPairs(this.schema, model, relationField); for (const pair of keyPairs) { if (!(pair.pk in entity)) { - throw new QueryError( - `Field "${pair.pk}" not found in parent created data` - ); + throw new QueryError(`Field "${pair.pk}" not found in parent created data`); } Object.assign(parentFkFields, { [pair.fk]: (entity as any)[pair.pk], @@ -709,9 +560,7 @@ export abstract class BaseOperationHandler { return parentFkFields; } - private async handleManyToManyRelation< - Action extends 'connect' | 'disconnect' - >( + private async handleManyToManyRelation( kysely: ToKysely, action: Action, leftModel: string, @@ -720,12 +569,8 @@ export abstract class BaseOperationHandler { rightModel: string, rightField: string, rightEntity: any, - joinTable: string - ): Promise< - Action extends 'connect' - ? UpdateResult | undefined - : DeleteResult | undefined - > { + joinTable: string, + ): Promise { const sortedRecords = [ { model: leftModel, @@ -741,14 +586,8 @@ export abstract class BaseOperationHandler { const firstIds = getIdFields(this.schema, sortedRecords[0]!.model); const secondIds = getIdFields(this.schema, sortedRecords[1]!.model); - invariant( - firstIds.length === 1, - 'many-to-many relation must have exactly one id field' - ); - invariant( - secondIds.length === 1, - 'many-to-many relation must have exactly one id field' - ); + invariant(firstIds.length === 1, 'many-to-many relation must have exactly one id field'); + invariant(secondIds.length === 1, 'many-to-many relation must have exactly one id field'); // Prisma's convention for many-to-many: fk fields are named "A" and "B" if (action === 'connect') { @@ -765,35 +604,15 @@ export abstract class BaseOperationHandler { const eb = expressionBuilder(); const result = await kysely .deleteFrom(joinTable as any) - .where( - eb( - `${joinTable}.A`, - '=', - sortedRecords[0]!.entity[firstIds[0]!] - ) - ) - .where( - eb( - `${joinTable}.B`, - '=', - sortedRecords[1]!.entity[secondIds[0]!] - ) - ) + .where(eb(`${joinTable}.A`, '=', sortedRecords[0]!.entity[firstIds[0]!])) + .where(eb(`${joinTable}.B`, '=', sortedRecords[1]!.entity[secondIds[0]!])) .execute(); return result[0] as any; } } - private resetManyToManyRelation( - kysely: ToKysely, - model: GetModels, - field: string, - parentIds: any - ) { - invariant( - Object.keys(parentIds).length === 1, - 'parentIds must have exactly one field' - ); + private resetManyToManyRelation(kysely: ToKysely, model: GetModels, field: string, parentIds: any) { + invariant(Object.keys(parentIds).length === 1, 'parentIds must have exactly one field'); const parentId = Object.values(parentIds)[0]!; const m2m = getManyToManyRelation(this.schema, model, field); @@ -806,11 +625,7 @@ export abstract class BaseOperationHandler { .execute(); } - private async processOwnedRelation( - kysely: ToKysely, - relationField: FieldDef, - payload: any - ) { + private async processOwnedRelation(kysely: ToKysely, relationField: FieldDef, payload: any) { if (!payload) { return; } @@ -824,54 +639,28 @@ export abstract class BaseOperationHandler { } switch (action) { case 'create': { - const created = await this.create( - kysely, - relationModel, - subPayload - ); + const created = await this.create(kysely, relationModel, subPayload); // extract id fields and return as foreign key values - result = getIdValues( - this.schema, - relationField.type, - created - ); + result = getIdValues(this.schema, relationField.type, created); break; } case 'connect': { - const referencedPkFields = - relationField.relation!.references!; - invariant( - referencedPkFields, - 'relation must have fields info' - ); - const extractedFks = extractFields( - subPayload, - referencedPkFields - ); - if ( - Object.keys(extractedFks).length === - referencedPkFields.length - ) { + const referencedPkFields = relationField.relation!.references!; + invariant(referencedPkFields, 'relation must have fields info'); + const extractedFks = extractFields(subPayload, referencedPkFields); + if (Object.keys(extractedFks).length === referencedPkFields.length) { // payload contains all referenced pk fields, we can // directly use it to connect the relation result = extractedFks; } else { // read the relation entity and fetch the referenced pk fields - const relationEntity = await this.readUnique( - kysely, - relationModel, - { - where: subPayload, - select: fieldsToSelectObject( - referencedPkFields - ) as any, - } - ); + const relationEntity = await this.readUnique(kysely, relationModel, { + where: subPayload, + select: fieldsToSelectObject(referencedPkFields) as any, + }); if (!relationEntity) { - throw new NotFoundError( - `Could not find the entity for connect action` - ); + throw new NotFoundError(`Could not find the entity for connect action`); } result = relationEntity; } @@ -879,23 +668,11 @@ export abstract class BaseOperationHandler { } case 'connectOrCreate': { - const found = await this.exists( - kysely, - relationModel, - subPayload.where - ); + const found = await this.exists(kysely, relationModel, subPayload.where); if (!found) { // create - const created = await this.create( - kysely, - relationModel, - subPayload.create - ); - result = getIdValues( - this.schema, - relationField.type, - created - ); + const created = await this.create(kysely, relationModel, subPayload.create); + result = getIdValues(this.schema, relationField.type, created); } else { // connect result = found; @@ -916,12 +693,9 @@ export abstract class BaseOperationHandler { contextModel: GetModels, relationFieldName: string, payload: any, - parentEntity: any + parentEntity: any, ) { - const relationFieldDef = this.requireField( - contextModel, - relationFieldName - ); + const relationFieldDef = this.requireField(contextModel, relationFieldName); const relationModel = relationFieldDef.type as GetModels; const tasks: Promise[] = []; @@ -938,24 +712,19 @@ export abstract class BaseOperationHandler { model: contextModel, field: relationFieldName, ids: parentEntity, - }) - ) + }), + ), ); break; } case 'connect': { tasks.push( - this.connectRelation( - kysely, - relationModel, - subPayload, - { - model: contextModel, - field: relationFieldName, - ids: parentEntity, - } - ) + this.connectRelation(kysely, relationModel, subPayload, { + model: contextModel, + field: relationFieldName, + ids: parentEntity, + }), ); break; } @@ -963,31 +732,20 @@ export abstract class BaseOperationHandler { case 'connectOrCreate': { tasks.push( ...enumerate(subPayload).map((item) => - this.exists(kysely, relationModel, item.where).then( - (found) => - !found - ? this.create( - kysely, - relationModel, - item.create, - { - model: contextModel, - field: relationFieldName, - ids: parentEntity, - } - ) - : this.connectRelation( - kysely, - relationModel, - found, - { - model: contextModel, - field: relationFieldName, - ids: parentEntity, - } - ) - ) - ) + this.exists(kysely, relationModel, item.where).then((found) => + !found + ? this.create(kysely, relationModel, item.create, { + model: contextModel, + field: relationFieldName, + ids: parentEntity, + }) + : this.connectRelation(kysely, relationModel, found, { + model: contextModel, + field: relationFieldName, + ids: parentEntity, + }), + ), + ), ); break; } @@ -1002,13 +760,13 @@ export abstract class BaseOperationHandler { protected async createMany< ReturnData extends boolean, - Result = ReturnData extends true ? unknown[] : { count: number } + Result = ReturnData extends true ? unknown[] : { count: number }, >( kysely: ToKysely, model: GetModels, input: { data: any; skipDuplicates?: boolean }, returnData: ReturnData, - fromRelation?: FromRelationContext + fromRelation?: FromRelationContext, ): Promise { const modelDef = this.requireModel(model); @@ -1017,12 +775,10 @@ export abstract class BaseOperationHandler { const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( this.schema, fromRelation.model, - fromRelation.field + fromRelation.field, ); if (ownedByModel) { - throw new QueryError( - 'incorrect relation hierarchy for createMany' - ); + throw new QueryError('incorrect relation hierarchy for createMany'); } relationKeyPairs = keyPairs; } @@ -1031,14 +787,8 @@ export abstract class BaseOperationHandler { const newItem: any = {}; for (const [name, value] of Object.entries(item)) { const fieldDef = this.requireField(model, name); - invariant( - !fieldDef.relation, - 'createMany does not support relations' - ); - newItem[name] = this.dialect.transformPrimitive( - value, - fieldDef.type as BuiltinType - ); + invariant(!fieldDef.relation, 'createMany does not support relations'); + newItem[name] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType); } if (fromRelation) { for (const { fk, pk } of relationKeyPairs) { @@ -1051,14 +801,12 @@ export abstract class BaseOperationHandler { const query = kysely .insertInto(model) .values(createData) - .$if(!!input.skipDuplicates, (qb) => - qb.onConflict((oc) => oc.doNothing()) - ) + .$if(!!input.skipDuplicates, (qb) => qb.onConflict((oc) => oc.doNothing())) .modifyEnd( this.makeContextComment({ model, operation: 'create', - }) + }), ); if (!returnData) { @@ -1076,20 +824,14 @@ export abstract class BaseOperationHandler { const values: any = clone(data); for (const field in fields) { if (!(field in data)) { - if ( - typeof fields[field]?.default === 'object' && - 'kind' in fields[field].default - ) { + if (typeof fields[field]?.default === 'object' && 'kind' in fields[field].default) { const generated = this.evalGenerator(fields[field].default); if (generated !== undefined) { values[field] = generated; } } else if (fields[field]?.updatedAt) { // TODO: should this work at kysely level instead? - values[field] = this.dialect.transformPrimitive( - new Date(), - 'DateTime' - ); + values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime'); } } } @@ -1105,14 +847,14 @@ export abstract class BaseOperationHandler { ExpressionUtils.isLiteral(defaultValue.args?.[0]) && defaultValue.args[0].value === 7 ? uuid.v7() - : uuid.v4() + : uuid.v4(), ) .with('nanoid', () => defaultValue.args?.[0] && ExpressionUtils.isLiteral(defaultValue.args[0]) && typeof defaultValue.args[0].value === 'number' ? nanoid(defaultValue.args[0].value) - : nanoid() + : nanoid(), ) .with('ulid', () => ulid()) .otherwise(() => undefined); @@ -1139,7 +881,7 @@ export abstract class BaseOperationHandler { data: any, fromRelation?: FromRelationContext, allowRelationUpdate = true, - throwIfNotFound = true + throwIfNotFound = true, ) { if (!data || typeof data !== 'object') { throw new InternalError('data must be an object'); @@ -1149,27 +891,18 @@ export abstract class BaseOperationHandler { let m2m: ReturnType = undefined; if (fromRelation) { - m2m = getManyToManyRelation( - this.schema, - fromRelation.model, - fromRelation.field - ); + m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); if (!m2m) { // merge foreign key conditions from the relation - const { ownedByModel, keyPairs } = - getRelationForeignKeyFieldPairs( - this.schema, - fromRelation.model, - fromRelation.field - ); + const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( + this.schema, + fromRelation.model, + fromRelation.field, + ); if (ownedByModel) { - const fromEntity = await this.readUnique( - kysely, - fromRelation.model as GetModels, - { - where: fromRelation.ids, - } - ); + const fromEntity = await this.readUnique(kysely, fromRelation.model as GetModels, { + where: fromRelation.ids, + }); for (const { fk, pk } of keyPairs) { parentWhere[pk] = fromEntity[fk]; } @@ -1180,10 +913,7 @@ export abstract class BaseOperationHandler { } } else { // many-to-many relation, filter for parent with "some" - const fromRelationFieldDef = this.requireField( - fromRelation.model, - fromRelation.field - ); + const fromRelationFieldDef = this.requireField(fromRelation.model, fromRelation.field); invariant(fromRelationFieldDef.relation?.opposite); parentWhere[fromRelationFieldDef.relation.opposite] = { some: fromRelation.ids, @@ -1191,16 +921,9 @@ export abstract class BaseOperationHandler { } } - let combinedWhere: WhereInput< - Schema, - GetModels, - false - > = where ?? {}; + let combinedWhere: WhereInput, false> = where ?? {}; if (Object.keys(parentWhere).length > 0) { - combinedWhere = - Object.keys(combinedWhere).length > 0 - ? { AND: [parentWhere, combinedWhere] } - : parentWhere; + combinedWhere = Object.keys(combinedWhere).length > 0 ? { AND: [parentWhere, combinedWhere] } : parentWhere; } // fill in automatically updated fields @@ -1211,10 +934,7 @@ export abstract class BaseOperationHandler { if (finalData === data) { finalData = clone(data); } - finalData[fieldName] = this.dialect.transformPrimitive( - new Date(), - 'DateTime' - ); + finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime'); } } @@ -1234,22 +954,10 @@ export abstract class BaseOperationHandler { for (const field in finalData) { const fieldDef = this.requireField(model, field); - if ( - isScalarField(this.schema, model, field) || - isForeignKeyField(this.schema, model, field) - ) { - if ( - this.isNumericField(fieldDef) && - typeof finalData[field] === 'object' && - finalData[field] - ) { + if (isScalarField(this.schema, model, field) || isForeignKeyField(this.schema, model, field)) { + if (this.isNumericField(fieldDef) && typeof finalData[field] === 'object' && finalData[field]) { // numeric fields incremental updates - updateFields[field] = this.transformIncrementalUpdate( - model, - field, - fieldDef, - finalData[field] - ); + updateFields[field] = this.transformIncrementalUpdate(model, field, fieldDef, finalData[field]); continue; } @@ -1260,24 +968,14 @@ export abstract class BaseOperationHandler { finalData[field] ) { // scalar list updates - updateFields[field] = this.transformScalarListUpdate( - model, - field, - fieldDef, - finalData[field] - ); + updateFields[field] = this.transformScalarListUpdate(model, field, fieldDef, finalData[field]); continue; } - updateFields[field] = this.dialect.transformPrimitive( - finalData[field], - fieldDef.type as BuiltinType - ); + updateFields[field] = this.dialect.transformPrimitive(finalData[field], fieldDef.type as BuiltinType); } else { if (!allowRelationUpdate) { - throw new QueryError( - `Relation update not allowed for field "${field}"` - ); + throw new QueryError(`Relation update not allowed for field "${field}"`); } if (!thisEntity) { thisEntity = await this.readUnique(kysely, model, { @@ -1299,31 +997,26 @@ export abstract class BaseOperationHandler { fieldDef, thisEntity, finalData[field], - throwIfNotFound + throwIfNotFound, ); } } if (Object.keys(updateFields).length === 0) { // nothing to update, simply read back - return ( - thisEntity ?? - (await this.readUnique(kysely, model, { where: combinedWhere })) - ); + return thisEntity ?? (await this.readUnique(kysely, model, { where: combinedWhere })); } else { const idFields = getIdFields(this.schema, model); const query = kysely .updateTable(model) - .where((eb) => - this.dialect.buildFilter(eb, model, model, combinedWhere) - ) + .where((eb) => this.dialect.buildFilter(eb, model, model, combinedWhere)) .set(updateFields) .returning(idFields as any) .modifyEnd( this.makeContextComment({ model, operation: 'update', - }) + }), ); const updatedEntity = await query.executeTakeFirst(); @@ -1353,26 +1046,17 @@ export abstract class BaseOperationHandler { model: GetModels, field: string, fieldDef: FieldDef, - payload: Record + payload: Record, ) { invariant( Object.keys(payload).length === 1, - 'Only one of "set", "increment", "decrement", "multiply", or "divide" can be provided' + 'Only one of "set", "increment", "decrement", "multiply", or "divide" can be provided', ); const key = Object.keys(payload)[0]; - const value = this.dialect.transformPrimitive( - payload[key!], - fieldDef.type as BuiltinType - ); + const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType); const eb = expressionBuilder(); - const fieldRef = buildFieldRef( - this.schema, - model, - field, - this.options, - eb - ); + const fieldRef = buildFieldRef(this.schema, model, field, this.options, eb); return match(key) .with('set', () => value) @@ -1381,9 +1065,7 @@ export abstract class BaseOperationHandler { .with('multiply', () => eb(fieldRef, '*', value)) .with('divide', () => eb(fieldRef, '/', value)) .otherwise(() => { - throw new InternalError( - `Invalid incremental update operation: ${key}` - ); + throw new InternalError(`Invalid incremental update operation: ${key}`); }); } @@ -1391,25 +1073,13 @@ export abstract class BaseOperationHandler { model: GetModels, field: string, fieldDef: FieldDef, - payload: Record + payload: Record, ) { - invariant( - Object.keys(payload).length === 1, - 'Only one of "set", "push" can be provided' - ); + invariant(Object.keys(payload).length === 1, 'Only one of "set", "push" can be provided'); const key = Object.keys(payload)[0]; - const value = this.dialect.transformPrimitive( - payload[key!], - fieldDef.type as BuiltinType - ); + const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType); const eb = expressionBuilder(); - const fieldRef = buildFieldRef( - this.schema, - model, - field, - this.options, - eb - ); + const fieldRef = buildFieldRef(this.schema, model, field, this.options, eb); return match(key) .with('set', () => value) @@ -1417,9 +1087,7 @@ export abstract class BaseOperationHandler { return eb(fieldRef, '||', eb.val(ensureArray(value))); }) .otherwise(() => { - throw new InternalError( - `Invalid array update operation: ${key}` - ); + throw new InternalError(`Invalid array update operation: ${key}`); }); } @@ -1427,23 +1095,20 @@ export abstract class BaseOperationHandler { return NUMERIC_FIELD_TYPES.includes(fieldDef.type) && !fieldDef.array; } - private makeContextComment(context: { - model: GetModels; - operation: CRUD; - }) { + private makeContextComment(context: { model: GetModels; operation: CRUD }) { return sql.raw(`${CONTEXT_COMMENT_PREFIX}${JSON.stringify(context)}`); } protected async updateMany< ReturnData extends boolean, - Result = ReturnData extends true ? unknown[] : { count: number } + Result = ReturnData extends true ? unknown[] : { count: number }, >( kysely: ToKysely, model: GetModels, where: any, data: any, limit: number | undefined, - returnData: ReturnData + returnData: ReturnData, ): Promise { if (typeof data !== 'object') { throw new InternalError('data must be an object'); @@ -1460,53 +1125,35 @@ export abstract class BaseOperationHandler { if (isRelationField(this.schema, model, field)) { continue; } - updateFields[field] = this.dialect.transformPrimitive( - data[field], - fieldDef.type as BuiltinType - ); + updateFields[field] = this.dialect.transformPrimitive(data[field], fieldDef.type as BuiltinType); } let query = kysely.updateTable(model).set(updateFields); if (limit === undefined) { - query = query.where((eb) => - this.dialect.buildFilter(eb, model, model, where) - ); + query = query.where((eb) => this.dialect.buildFilter(eb, model, model, where)); } else { if (this.dialect.supportsUpdateWithLimit) { - query = query - .where((eb) => - this.dialect.buildFilter(eb, model, model, where) - ) - .limit(limit!); + query = query.where((eb) => this.dialect.buildFilter(eb, model, model, where)).limit(limit!); } else { query = query.where((eb) => eb( eb.refTuple( // @ts-expect-error - ...this.buildIdFieldRefs(kysely, model) + ...this.buildIdFieldRefs(kysely, model), ), 'in', kysely .selectFrom(model) - .where((eb) => - this.dialect.buildFilter( - eb, - model, - model, - where - ) - ) + .where((eb) => this.dialect.buildFilter(eb, model, model, where)) .select(this.buildIdFieldRefs(kysely, model)) - .limit(limit!) - ) + .limit(limit!), + ), ); } } - query = query.modifyEnd( - this.makeContextComment({ model, operation: 'update' }) - ); + query = query.modifyEnd(this.makeContextComment({ model, operation: 'update' })); try { if (!returnData) { @@ -1519,16 +1166,11 @@ export abstract class BaseOperationHandler { } } catch (err) { const { sql, parameters } = query.compile(); - throw new QueryError( - `Error during updateMany: ${err}, sql: ${sql}, parameters: ${parameters}` - ); + throw new QueryError(`Error during updateMany: ${err}, sql: ${sql}, parameters: ${parameters}`); } } - private buildIdFieldRefs( - kysely: ToKysely, - model: GetModels - ) { + private buildIdFieldRefs(kysely: ToKysely, model: GetModels) { const idFields = getIdFields(this.schema, model); return idFields.map((f) => kysely.dynamic.ref(f)); } @@ -1540,7 +1182,7 @@ export abstract class BaseOperationHandler { fieldDef: FieldDef, parentIds: any, args: any, - throwIfNotFound: boolean + throwIfNotFound: boolean, ) { const tasks: Promise[] = []; const fieldModel = fieldDef.type as GetModels; @@ -1555,92 +1197,52 @@ export abstract class BaseOperationHandler { case 'create': { invariant( !Array.isArray(value) || fieldDef.array, - 'relation must be an array if create is an array' + 'relation must be an array if create is an array', ); tasks.push( - ...enumerate(value).map((item) => - this.create( - kysely, - fieldModel, - item, - fromRelationContext - ) - ) + ...enumerate(value).map((item) => this.create(kysely, fieldModel, item, fromRelationContext)), ); break; } case 'createMany': { - invariant( - fieldDef.array, - 'relation must be an array for createMany' - ); + invariant(fieldDef.array, 'relation must be an array for createMany'); tasks.push( this.createMany( kysely, fieldModel, value as { data: any; skipDuplicates: boolean }, false, - fromRelationContext - ) + fromRelationContext, + ), ); break; } case 'connect': { - tasks.push( - this.connectRelation( - kysely, - fieldModel, - value, - fromRelationContext - ) - ); + tasks.push(this.connectRelation(kysely, fieldModel, value, fromRelationContext)); break; } case 'connectOrCreate': { - tasks.push( - this.connectOrCreateRelation( - kysely, - fieldModel, - value, - fromRelationContext - ) - ); + tasks.push(this.connectOrCreateRelation(kysely, fieldModel, value, fromRelationContext)); break; } case 'disconnect': { - tasks.push( - this.disconnectRelation( - kysely, - fieldModel, - value, - fromRelationContext - ) - ); + tasks.push(this.disconnectRelation(kysely, fieldModel, value, fromRelationContext)); break; } case 'set': { invariant(fieldDef.array, 'relation must be an array'); - tasks.push( - this.setRelation( - kysely, - fieldModel, - value, - fromRelationContext - ) - ); + tasks.push(this.setRelation(kysely, fieldModel, value, fromRelationContext)); break; } case 'update': { tasks.push( - ...( - enumerate(value) as { where: any; data: any }[] - ).map((item) => { + ...(enumerate(value) as { where: any; data: any }[]).map((item) => { let where; let data; if ('where' in item) { @@ -1657,9 +1259,9 @@ export abstract class BaseOperationHandler { data, fromRelationContext, true, - throwIfNotFound + throwIfNotFound, ); - }) + }), ); break; } @@ -1680,65 +1282,34 @@ export abstract class BaseOperationHandler { item.update, fromRelationContext, true, - false + false, ); if (updated) { return updated; } else { - return this.create( - kysely, - fieldModel, - item.create, - fromRelationContext - ); + return this.create(kysely, fieldModel, item.create, fromRelationContext); } - }) + }), ); break; } case 'updateMany': { tasks.push( - ...( - enumerate(value) as { where: any; data: any }[] - ).map((item) => - this.update( - kysely, - fieldModel, - item.where, - item.data, - fromRelationContext, - false, - false - ) - ) + ...(enumerate(value) as { where: any; data: any }[]).map((item) => + this.update(kysely, fieldModel, item.where, item.data, fromRelationContext, false, false), + ), ); break; } case 'delete': { - tasks.push( - this.deleteRelation( - kysely, - fieldModel, - value, - fromRelationContext, - true - ) - ); + tasks.push(this.deleteRelation(kysely, fieldModel, value, fromRelationContext, true)); break; } case 'deleteMany': { - tasks.push( - this.deleteRelation( - kysely, - fieldModel, - value, - fromRelationContext, - false - ) - ); + tasks.push(this.deleteRelation(kysely, fieldModel, value, fromRelationContext, false)); break; } @@ -1757,18 +1328,14 @@ export abstract class BaseOperationHandler { kysely: ToKysely, model: GetModels, data: any, - fromRelation: FromRelationContext + fromRelation: FromRelationContext, ) { const _data = this.normalizeRelationManipulationInput(model, data); if (_data.length === 0) { return; } - const m2m = getManyToManyRelation( - this.schema, - fromRelation.model, - fromRelation.field - ); + const m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); if (m2m) { // handle many-to-many relation const actions = _data.map(async (d) => { @@ -1782,7 +1349,7 @@ export abstract class BaseOperationHandler { m2m.otherModel!, m2m.otherField!, ids, - m2m.joinTable + m2m.joinTable, ); }); const results = await Promise.all(actions); @@ -1795,16 +1362,13 @@ export abstract class BaseOperationHandler { const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( this.schema, fromRelation.model, - fromRelation.field + fromRelation.field, ); let updateResult: UpdateResult; if (ownedByModel) { // set parent fk directly - invariant( - _data.length === 1, - 'only one entity can be connected' - ); + invariant(_data.length === 1, 'only one entity can be connected'); const target = await this.readUnique(kysely, model, { where: _data[0], }); @@ -1820,44 +1384,30 @@ export abstract class BaseOperationHandler { ...acc, [fk]: target[pk], }), - {} as any - ) + {} as any, + ), ) .modifyEnd( this.makeContextComment({ model: fromRelation.model, operation: 'update', - }) + }), ); updateResult = await query.executeTakeFirstOrThrow(); } else { // disconnect current if it's a one-one relation - const relationFieldDef = this.requireField( - fromRelation.model, - fromRelation.field - ); + const relationFieldDef = this.requireField(fromRelation.model, fromRelation.field); if (!relationFieldDef.array) { const query = kysely .updateTable(model) - .where((eb) => - eb.and( - keyPairs.map(({ fk, pk }) => - eb(sql.ref(fk), '=', fromRelation.ids[pk]) - ) - ) - ) - .set( - keyPairs.reduce( - (acc, { fk }) => ({ ...acc, [fk]: null }), - {} as any - ) - ) + .where((eb) => eb.and(keyPairs.map(({ fk, pk }) => eb(sql.ref(fk), '=', fromRelation.ids[pk])))) + .set(keyPairs.reduce((acc, { fk }) => ({ ...acc, [fk]: null }), {} as any)) .modifyEnd( this.makeContextComment({ model: fromRelation.model, operation: 'update', - }) + }), ); await query.execute(); } @@ -1872,14 +1422,14 @@ export abstract class BaseOperationHandler { ...acc, [fk]: fromRelation.ids[pk], }), - {} as any - ) + {} as any, + ), ) .modifyEnd( this.makeContextComment({ model, operation: 'update', - }) + }), ); updateResult = await query.executeTakeFirstOrThrow(); } @@ -1896,7 +1446,7 @@ export abstract class BaseOperationHandler { kysely: ToKysely, model: GetModels, data: any, - fromRelation: FromRelationContext + fromRelation: FromRelationContext, ) { const _data = enumerate(data); if (_data.length === 0) { @@ -1907,16 +1457,11 @@ export abstract class BaseOperationHandler { _data.map(async ({ where, create }) => { const existing = await this.exists(kysely, model, where); if (existing) { - return this.connectRelation( - kysely, - model, - [where], - fromRelation - ); + return this.connectRelation(kysely, model, [where], fromRelation); } else { return this.create(kysely, model, create, fromRelation); } - }) + }), ); } @@ -1924,7 +1469,7 @@ export abstract class BaseOperationHandler { kysely: ToKysely, model: GetModels, data: any, - fromRelation: FromRelationContext + fromRelation: FromRelationContext, ) { let disconnectConditions: any[] = []; if (typeof data === 'boolean') { @@ -1934,10 +1479,7 @@ export abstract class BaseOperationHandler { disconnectConditions = [true]; } } else { - disconnectConditions = this.normalizeRelationManipulationInput( - model, - data - ); + disconnectConditions = this.normalizeRelationManipulationInput(model, data); if (disconnectConditions.length === 0) { return; @@ -1948,11 +1490,7 @@ export abstract class BaseOperationHandler { return; } - const m2m = getManyToManyRelation( - this.schema, - fromRelation.model, - fromRelation.field - ); + const m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); if (m2m) { // handle many-to-many relation const actions = disconnectConditions.map(async (d) => { @@ -1970,7 +1508,7 @@ export abstract class BaseOperationHandler { m2m.otherModel, m2m.otherField, ids, - m2m.joinTable + m2m.joinTable, ); }); await Promise.all(actions); @@ -1978,16 +1516,13 @@ export abstract class BaseOperationHandler { const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( this.schema, fromRelation.model, - fromRelation.field + fromRelation.field, ); const eb = expressionBuilder(); if (ownedByModel) { // set parent fk directly - invariant( - disconnectConditions.length === 1, - 'only one entity can be disconnected' - ); + invariant(disconnectConditions.length === 1, 'only one entity can be disconnected'); const condition = disconnectConditions[0]; const query = kysely .updateTable(fromRelation.model) @@ -2003,28 +1538,16 @@ export abstract class BaseOperationHandler { eb .selectFrom(model) .select(keyPairs.map(({ pk }) => pk)) - .where( - this.dialect.buildFilter( - eb, - model, - model, - condition - ) - ) - ) - ) - ) - .set( - keyPairs.reduce( - (acc, { fk }) => ({ ...acc, [fk]: null }), - {} as any - ) + .where(this.dialect.buildFilter(eb, model, model, condition)), + ), + ), ) + .set(keyPairs.reduce((acc, { fk }) => ({ ...acc, [fk]: null }), {} as any)) .modifyEnd( this.makeContextComment({ model: fromRelation.model, operation: 'update', - }) + }), ); await query.executeTakeFirstOrThrow(); } else { @@ -2034,29 +1557,17 @@ export abstract class BaseOperationHandler { .where( eb.and([ // fk filter - eb.and( - Object.fromEntries( - keyPairs.map(({ fk, pk }) => [ - fk, - fromRelation.ids[pk], - ]) - ) - ), + eb.and(Object.fromEntries(keyPairs.map(({ fk, pk }) => [fk, fromRelation.ids[pk]]))), // merge extra disconnect conditions eb.or(disconnectConditions.map((d) => eb.and(d))), - ]) - ) - .set( - keyPairs.reduce( - (acc, { fk }) => ({ ...acc, [fk]: null }), - {} as any - ) + ]), ) + .set(keyPairs.reduce((acc, { fk }) => ({ ...acc, [fk]: null }), {} as any)) .modifyEnd( this.makeContextComment({ model, operation: 'update', - }) + }), ); await query.executeTakeFirstOrThrow(); } @@ -2067,26 +1578,17 @@ export abstract class BaseOperationHandler { kysely: ToKysely, model: GetModels, data: any, - fromRelation: FromRelationContext + fromRelation: FromRelationContext, ) { const _data = this.normalizeRelationManipulationInput(model, data); - const m2m = getManyToManyRelation( - this.schema, - fromRelation.model, - fromRelation.field - ); + const m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); if (m2m) { // handle many-to-many relation // reset for the parent - await this.resetManyToManyRelation( - kysely, - fromRelation.model, - fromRelation.field, - fromRelation.ids - ); + await this.resetManyToManyRelation(kysely, fromRelation.model, fromRelation.field, fromRelation.ids); // connect new entities const actions = _data.map(async (d) => { @@ -2100,7 +1602,7 @@ export abstract class BaseOperationHandler { m2m.otherModel, m2m.otherField, ids, - m2m.joinTable + m2m.joinTable, ); }); const results = await Promise.all(actions); @@ -2113,13 +1615,11 @@ export abstract class BaseOperationHandler { const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( this.schema, fromRelation.model, - fromRelation.field + fromRelation.field, ); if (ownedByModel) { - throw new InternalError( - 'relation can only be set from the non-owning side' - ); + throw new InternalError('relation can only be set from the non-owning side'); } const fkConditions = keyPairs.reduce( @@ -2127,7 +1627,7 @@ export abstract class BaseOperationHandler { ...acc, [fk]: fromRelation.ids[pk], }), - {} as any + {} as any, ); // disconnect @@ -2139,19 +1639,14 @@ export abstract class BaseOperationHandler { eb.and(fkConditions), // exclude entities to be connected eb.not(eb.or(_data.map((d) => eb.and(d)))), - ]) - ) - .set( - keyPairs.reduce( - (acc, { fk }) => ({ ...acc, [fk]: null }), - {} as any - ) + ]), ) + .set(keyPairs.reduce((acc, { fk }) => ({ ...acc, [fk]: null }), {} as any)) .modifyEnd( this.makeContextComment({ model, operation: 'update', - }) + }), ); await query.execute(); @@ -2166,14 +1661,14 @@ export abstract class BaseOperationHandler { ...acc, [fk]: fromRelation.ids[pk], }), - {} as any - ) + {} as any, + ), ) .modifyEnd( this.makeContextComment({ model, operation: 'update', - }) + }), ); const r = await query.executeTakeFirstOrThrow(); @@ -2191,7 +1686,7 @@ export abstract class BaseOperationHandler { model: GetModels, data: any, fromRelation: FromRelationContext, - throwForNotFound: boolean + throwForNotFound: boolean, ) { let deleteConditions: any[] = []; let expectedDeleteCount: number; @@ -2203,10 +1698,7 @@ export abstract class BaseOperationHandler { expectedDeleteCount = 1; } } else { - deleteConditions = this.normalizeRelationManipulationInput( - model, - data - ); + deleteConditions = this.normalizeRelationManipulationInput(model, data); if (deleteConditions.length === 0) { return; } @@ -2214,18 +1706,11 @@ export abstract class BaseOperationHandler { } let deleteResult: { count: number }; - const m2m = getManyToManyRelation( - this.schema, - fromRelation.model, - fromRelation.field - ); + const m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); if (m2m) { // handle many-to-many relation - const fieldDef = this.requireField( - fromRelation.model, - fromRelation.field - ); + const fieldDef = this.requireField(fromRelation.model, fromRelation.field); invariant(fieldDef.relation?.opposite); deleteResult = await this.delete( @@ -2244,31 +1729,24 @@ export abstract class BaseOperationHandler { ], }, undefined, - false + false, ); } else { const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( this.schema, fromRelation.model, - fromRelation.field + fromRelation.field, ); if (ownedByModel) { - const fromEntity = await this.readUnique( - kysely, - fromRelation.model as GetModels, - { - where: fromRelation.ids, - } - ); + const fromEntity = await this.readUnique(kysely, fromRelation.model as GetModels, { + where: fromRelation.ids, + }); if (!fromEntity) { throw new NotFoundError(model); } - const fieldDef = this.requireField( - fromRelation.model, - fromRelation.field - ); + const fieldDef = this.requireField(fromRelation.model, fromRelation.field); invariant(fieldDef.relation?.opposite); deleteResult = await this.delete( kysely, @@ -2276,19 +1754,14 @@ export abstract class BaseOperationHandler { { AND: [ // filter for parent - Object.fromEntries( - keyPairs.map(({ fk, pk }) => [ - pk, - fromEntity[fk], - ]) - ), + Object.fromEntries(keyPairs.map(({ fk, pk }) => [pk, fromEntity[fk]])), { OR: deleteConditions, }, ], }, undefined, - false + false, ); } else { deleteResult = await this.delete( @@ -2296,19 +1769,14 @@ export abstract class BaseOperationHandler { model, { AND: [ - Object.fromEntries( - keyPairs.map(({ fk, pk }) => [ - fk, - fromRelation.ids[pk], - ]) - ), + Object.fromEntries(keyPairs.map(({ fk, pk }) => [fk, fromRelation.ids[pk]])), { OR: deleteConditions, }, ], }, undefined, - false + false, ); } } @@ -2320,75 +1788,54 @@ export abstract class BaseOperationHandler { } } - private normalizeRelationManipulationInput( - model: GetModels, - data: any - ) { - return enumerate(data).map((item) => - flattenCompoundUniqueFilters(this.schema, model, item) - ); + private normalizeRelationManipulationInput(model: GetModels, data: any) { + return enumerate(data).map((item) => flattenCompoundUniqueFilters(this.schema, model, item)); } // #endregion protected async delete< ReturnData extends boolean, - Result = ReturnData extends true ? unknown[] : { count: number } + Result = ReturnData extends true ? unknown[] : { count: number }, >( kysely: ToKysely, model: GetModels, where: any, limit: number | undefined, - returnData: ReturnData + returnData: ReturnData, ): Promise { let query = kysely.deleteFrom(model); if (limit === undefined) { - query = query.where((eb) => - this.dialect.buildFilter(eb, model, model, where) - ); + query = query.where((eb) => this.dialect.buildFilter(eb, model, model, where)); } else { if (this.dialect.supportsDeleteWithLimit) { - query = query - .where((eb) => - this.dialect.buildFilter(eb, model, model, where) - ) - .limit(limit!); + query = query.where((eb) => this.dialect.buildFilter(eb, model, model, where)).limit(limit!); } else { query = query.where((eb) => eb( eb.refTuple( // @ts-expect-error - ...this.buildIdFieldRefs(kysely, model) + ...this.buildIdFieldRefs(kysely, model), ), 'in', kysely .selectFrom(model) - .where((eb) => - this.dialect.buildFilter( - eb, - model, - model, - where - ) - ) + .where((eb) => this.dialect.buildFilter(eb, model, model, where)) .select(this.buildIdFieldRefs(kysely, model)) - .limit(limit!) - ) + .limit(limit!), + ), ); } } - query = query.modifyEnd( - this.makeContextComment({ model, operation: 'delete' }) - ); + query = query.modifyEnd(this.makeContextComment({ model, operation: 'delete' })); if (returnData) { const result = await query.execute(); return result as Result; } else { - const result = - (await query.executeTakeFirstOrThrow()) as DeleteResult; + const result = (await query.executeTakeFirstOrThrow()) as DeleteResult; return { count: Number(result.numDeletedRows), } as Result; @@ -2403,10 +1850,7 @@ export abstract class BaseOperationHandler { }, {} as any); } - protected trimResult( - data: any, - args: SelectIncludeOmit, boolean> - ) { + protected trimResult(data: any, args: SelectIncludeOmit, boolean>) { if (!args.select) { return data; } @@ -2416,10 +1860,7 @@ export abstract class BaseOperationHandler { }, {} as any); } - protected needReturnRelations( - model: string, - args: SelectIncludeOmit, boolean> - ) { + protected needReturnRelations(model: string, args: SelectIncludeOmit, boolean>) { let returnRelation = false; if (args.include) { @@ -2433,33 +1874,22 @@ export abstract class BaseOperationHandler { return returnRelation; } - protected async safeTransaction( - callback: (tx: ToKysely) => Promise - ) { + protected async safeTransaction(callback: (tx: ToKysely) => Promise) { if (this.kysely.isTransaction) { return callback(this.kysely); } else { - return this.kysely - .transaction() - .setIsolationLevel('repeatable read') - .execute(callback); + return this.kysely.transaction().setIsolationLevel('repeatable read').execute(callback); } } // Given a unique filter of a model, return the entity ids by trying to // reused the filter if it's a complete id filter (without extra fields) // otherwise, read the entity by the filter - private getEntityIds( - kysely: ToKysely, - model: GetModels, - uniqueFilter: any - ) { + private getEntityIds(kysely: ToKysely, model: GetModels, uniqueFilter: any) { const idFields: string[] = getIdFields(this.schema, model); if ( // all id fields are provided - idFields.every( - (f) => f in uniqueFilter && uniqueFilter[f] !== undefined - ) && + idFields.every((f) => f in uniqueFilter && uniqueFilter[f] !== undefined) && // no non-id filter exists Object.keys(uniqueFilter).every((k) => idFields.includes(k)) ) { diff --git a/packages/runtime/src/client/crud/operations/count.ts b/packages/runtime/src/client/crud/operations/count.ts index 1a8cb02e..f454762b 100644 --- a/packages/runtime/src/client/crud/operations/count.ts +++ b/packages/runtime/src/client/crud/operations/count.ts @@ -2,33 +2,17 @@ import { sql } from 'kysely'; import type { SchemaDef } from '../../../schema'; import { BaseOperationHandler } from './base'; -export class CountOperationHandler< - Schema extends SchemaDef -> extends BaseOperationHandler { +export class CountOperationHandler extends BaseOperationHandler { async handle(_operation: 'count', args: unknown | undefined) { - const validatedArgs = this.inputValidator.validateCountArgs( - this.model, - args - ); + const validatedArgs = this.inputValidator.validateCountArgs(this.model, args); let query = this.kysely.selectFrom((eb) => { // nested query for filtering and pagination let subQuery = eb .selectFrom(this.model) .selectAll() - .where((eb1) => - this.dialect.buildFilter( - eb1, - this.model, - this.model, - validatedArgs?.where - ) - ); - subQuery = this.dialect.buildSkipTake( - subQuery, - validatedArgs?.skip, - validatedArgs?.take - ); + .where((eb1) => this.dialect.buildFilter(eb1, this.model, this.model, validatedArgs?.where)); + subQuery = this.dialect.buildSkipTake(subQuery, validatedArgs?.skip, validatedArgs?.take); return subQuery.as('$sub'); }); @@ -38,21 +22,14 @@ export class CountOperationHandler< Object.keys(validatedArgs.select!).map((key) => key === '_all' ? eb.cast(eb.fn.countAll(), 'integer').as('_all') - : eb - .cast( - eb.fn.count(sql.ref(`$sub.${key}`)), - 'integer' - ) - .as(key) - ) + : eb.cast(eb.fn.count(sql.ref(`$sub.${key}`)), 'integer').as(key), + ), ); return query.executeTakeFirstOrThrow(); } else { // simple count all - query = query.select((eb) => - eb.cast(eb.fn.countAll(), 'integer').as('count') - ); + query = query.select((eb) => eb.cast(eb.fn.countAll(), 'integer').as('count')); const result = await query.executeTakeFirstOrThrow(); return (result as any).count as number; } diff --git a/packages/runtime/src/client/crud/operations/create.ts b/packages/runtime/src/client/crud/operations/create.ts index 5ecd5283..1b6c9288 100644 --- a/packages/runtime/src/client/crud/operations/create.ts +++ b/packages/runtime/src/client/crud/operations/create.ts @@ -1,39 +1,20 @@ import { match } from 'ts-pattern'; import { RejectedByPolicyError } from '../../../plugins/policy/errors'; import type { GetModels, SchemaDef } from '../../../schema'; -import type { - CreateArgs, - CreateManyAndReturnArgs, - CreateManyArgs, - WhereInput, -} from '../../crud-types'; +import type { CreateArgs, CreateManyAndReturnArgs, CreateManyArgs, WhereInput } from '../../crud-types'; import { getIdValues } from '../../query-utils'; import { BaseOperationHandler } from './base'; -export class CreateOperationHandler< - Schema extends SchemaDef -> extends BaseOperationHandler { - async handle( - operation: 'create' | 'createMany' | 'createManyAndReturn', - args: unknown | undefined - ) { +export class CreateOperationHandler extends BaseOperationHandler { + async handle(operation: 'create' | 'createMany' | 'createManyAndReturn', args: unknown | undefined) { return match(operation) - .with('create', () => - this.runCreate( - this.inputValidator.validateCreateArgs(this.model, args) - ) - ) + .with('create', () => this.runCreate(this.inputValidator.validateCreateArgs(this.model, args))) .with('createMany', () => { - return this.runCreateMany( - this.inputValidator.validateCreateManyArgs(this.model, args) - ); + return this.runCreateMany(this.inputValidator.validateCreateManyArgs(this.model, args)); }) .with('createManyAndReturn', () => { return this.runCreateManyAndReturn( - this.inputValidator.validateCreateManyAndReturnArgs( - this.model, - args - ) + this.inputValidator.validateCreateManyAndReturnArgs(this.model, args), ); }) .exhaustive(); @@ -47,19 +28,16 @@ export class CreateOperationHandler< select: args.select, include: args.include, omit: args.omit, - where: getIdValues( - this.schema, - this.model, - createResult - ) as WhereInput, false>, + where: getIdValues(this.schema, this.model, createResult) as WhereInput< + Schema, + GetModels, + false + >, }); }); if (!result && this.hasPolicyEnabled) { - throw new RejectedByPolicyError( - this.model, - `result is not allowed to be read back` - ); + throw new RejectedByPolicyError(this.model, `result is not allowed to be read back`); } return result; @@ -72,29 +50,19 @@ export class CreateOperationHandler< return this.createMany(this.kysely, this.model, args, false); } - private async runCreateManyAndReturn( - args?: CreateManyAndReturnArgs> - ) { + private async runCreateManyAndReturn(args?: CreateManyAndReturnArgs>) { if (args === undefined) { return []; } // TODO: avoid using transaction for simple create return this.safeTransaction(async (tx) => { - const createResult = await this.createMany( - tx, - this.model, - args, - true - ); + const createResult = await this.createMany(tx, this.model, args, true); return this.read(tx, this.model, { select: args.select, omit: args.omit, where: { - OR: createResult.map( - (item) => - getIdValues(this.schema, this.model, item) as any - ), + OR: createResult.map((item) => getIdValues(this.schema, this.model, item) as any), } as any, // TODO: fix type }); }); diff --git a/packages/runtime/src/client/crud/operations/delete.ts b/packages/runtime/src/client/crud/operations/delete.ts index 85595aa1..6933b1a8 100644 --- a/packages/runtime/src/client/crud/operations/delete.ts +++ b/packages/runtime/src/client/crud/operations/delete.ts @@ -4,30 +4,15 @@ import type { DeleteArgs, DeleteManyArgs } from '../../crud-types'; import { NotFoundError } from '../../errors'; import { BaseOperationHandler } from './base'; -export class DeleteOperationHandler< - Schema extends SchemaDef -> extends BaseOperationHandler { - async handle( - operation: 'delete' | 'deleteMany', - args: unknown | undefined - ) { +export class DeleteOperationHandler extends BaseOperationHandler { + async handle(operation: 'delete' | 'deleteMany', args: unknown | undefined) { return match(operation) - .with('delete', () => - this.runDelete( - this.inputValidator.validateDeleteArgs(this.model, args) - ) - ) - .with('deleteMany', () => - this.runDeleteMany( - this.inputValidator.validateDeleteManyArgs(this.model, args) - ) - ) + .with('delete', () => this.runDelete(this.inputValidator.validateDeleteArgs(this.model, args))) + .with('deleteMany', () => this.runDeleteMany(this.inputValidator.validateDeleteManyArgs(this.model, args))) .exhaustive(); } - async runDelete( - args: DeleteArgs> - ) { + async runDelete(args: DeleteArgs>) { const existing = await this.readUnique(this.kysely, this.model, { select: args.select, include: args.include, @@ -37,31 +22,15 @@ export class DeleteOperationHandler< if (!existing) { throw new NotFoundError(this.model); } - const result = await this.delete( - this.kysely, - this.model, - args.where, - undefined, - false - ); + const result = await this.delete(this.kysely, this.model, args.where, undefined, false); if (result.count === 0) { throw new NotFoundError(this.model); } return existing; } - async runDeleteMany( - args: - | DeleteManyArgs> - | undefined - ) { - const result = await this.delete( - this.kysely, - this.model, - args?.where, - args?.limit, - false - ); + async runDeleteMany(args: DeleteManyArgs> | undefined) { + const result = await this.delete(this.kysely, this.model, args?.where, args?.limit, false); return result; } } diff --git a/packages/runtime/src/client/crud/operations/find.ts b/packages/runtime/src/client/crud/operations/find.ts index 5df64cb9..8a868fad 100644 --- a/packages/runtime/src/client/crud/operations/find.ts +++ b/packages/runtime/src/client/crud/operations/find.ts @@ -2,32 +2,21 @@ import type { GetModels, SchemaDef } from '../../../schema'; import type { FindArgs } from '../../crud-types'; import { BaseOperationHandler, type CrudOperation } from './base'; -export class FindOperationHandler< - Schema extends SchemaDef -> extends BaseOperationHandler { - async handle( - operation: CrudOperation, - args: unknown, - validateArgs = true - ): Promise { +export class FindOperationHandler extends BaseOperationHandler { + async handle(operation: CrudOperation, args: unknown, validateArgs = true): Promise { // parse args const parsedArgs = validateArgs - ? this.inputValidator.validateFindArgs( - this.model, - operation === 'findUnique', - args - ) + ? this.inputValidator.validateFindArgs(this.model, operation === 'findUnique', args) : args; // run query const result = await this.read( this.client.$qb, this.model, - parsedArgs as FindArgs, true> + parsedArgs as FindArgs, true>, ); - const finalResult = - operation === 'findMany' ? result : result[0] ?? null; + const finalResult = operation === 'findMany' ? result : (result[0] ?? null); return finalResult; } } diff --git a/packages/runtime/src/client/crud/operations/group-by.ts b/packages/runtime/src/client/crud/operations/group-by.ts index 827fbbc1..c59b1f7f 100644 --- a/packages/runtime/src/client/crud/operations/group-by.ts +++ b/packages/runtime/src/client/crud/operations/group-by.ts @@ -4,14 +4,9 @@ import type { SchemaDef } from '../../../schema'; import { getField } from '../../query-utils'; import { BaseOperationHandler } from './base'; -export class GroupByeOperationHandler< - Schema extends SchemaDef -> extends BaseOperationHandler { +export class GroupByeOperationHandler extends BaseOperationHandler { async handle(_operation: 'groupBy', args: unknown | undefined) { - const validatedArgs = this.inputValidator.validateGroupByArgs( - this.model, - args - ); + const validatedArgs = this.inputValidator.validateGroupByArgs(this.model, args); let query = this.kysely.selectFrom((eb) => { // nested query for filtering and pagination @@ -20,14 +15,7 @@ export class GroupByeOperationHandler< let subQuery = eb .selectFrom(this.model) .selectAll() - .where((eb1) => - this.dialect.buildFilter( - eb1, - this.model, - this.model, - validatedArgs?.where - ) - ); + .where((eb1) => this.dialect.buildFilter(eb1, this.model, this.model, validatedArgs?.where)); // skip & take const skip = validatedArgs?.skip; @@ -46,40 +34,23 @@ export class GroupByeOperationHandler< this.model, undefined, skip !== undefined || take !== undefined, - negateOrderBy + negateOrderBy, ); return subQuery.as('$sub'); }); - const bys = - typeof validatedArgs.by === 'string' - ? [validatedArgs.by] - : (validatedArgs.by as string[]); + const bys = typeof validatedArgs.by === 'string' ? [validatedArgs.by] : (validatedArgs.by as string[]); query = query.groupBy(bys as any); // orderBy if (validatedArgs.orderBy) { - query = this.dialect.buildOrderBy( - query, - this.model, - '$sub', - validatedArgs.orderBy, - false, - false - ); + query = this.dialect.buildOrderBy(query, this.model, '$sub', validatedArgs.orderBy, false, false); } if (validatedArgs.having) { - query = query.having((eb1) => - this.dialect.buildFilter( - eb1, - this.model, - '$sub', - validatedArgs.having - ) - ); + query = query.having((eb1) => this.dialect.buildFilter(eb1, this.model, '$sub', validatedArgs.having)); } // select all by fields @@ -92,28 +63,17 @@ export class GroupByeOperationHandler< switch (key) { case '_count': { if (value === true) { - query = query.select((eb) => - eb.cast(eb.fn.countAll(), 'integer').as('_count') - ); + query = query.select((eb) => eb.cast(eb.fn.countAll(), 'integer').as('_count')); } else { Object.entries(value).forEach(([field, val]) => { if (val === true) { if (field === '_all') { query = query.select((eb) => - eb - .cast(eb.fn.countAll(), 'integer') - .as(`_count._all`) + eb.cast(eb.fn.countAll(), 'integer').as(`_count._all`), ); } else { query = query.select((eb) => - eb - .cast( - eb.fn.count( - sql.ref(`$sub.${field}`) - ), - 'integer' - ) - .as(`${key}.${field}`) + eb.cast(eb.fn.count(sql.ref(`$sub.${field}`)), 'integer').as(`${key}.${field}`), ); } } @@ -135,9 +95,7 @@ export class GroupByeOperationHandler< .with('_max', () => eb.fn.max) .with('_min', () => eb.fn.min) .exhaustive(); - return fn(sql.ref(`$sub.${field}`)).as( - `${key}.${field}` - ); + return fn(sql.ref(`$sub.${field}`)).as(`${key}.${field}`); }); } }); diff --git a/packages/runtime/src/client/crud/operations/update.ts b/packages/runtime/src/client/crud/operations/update.ts index 0c167e10..4771b071 100644 --- a/packages/runtime/src/client/crud/operations/update.ts +++ b/packages/runtime/src/client/crud/operations/update.ts @@ -1,75 +1,35 @@ import { match } from 'ts-pattern'; import { RejectedByPolicyError } from '../../../plugins/policy/errors'; import type { GetModels, SchemaDef } from '../../../schema'; -import type { - UpdateArgs, - UpdateManyAndReturnArgs, - UpdateManyArgs, - UpsertArgs, - WhereInput, -} from '../../crud-types'; +import type { UpdateArgs, UpdateManyAndReturnArgs, UpdateManyArgs, UpsertArgs, WhereInput } from '../../crud-types'; import { getIdValues } from '../../query-utils'; import { BaseOperationHandler } from './base'; -export class UpdateOperationHandler< - Schema extends SchemaDef -> extends BaseOperationHandler { - async handle( - operation: 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert', - args: unknown - ) { +export class UpdateOperationHandler extends BaseOperationHandler { + async handle(operation: 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert', args: unknown) { return match(operation) - .with('update', () => - this.runUpdate( - this.inputValidator.validateUpdateArgs(this.model, args) - ) - ) - .with('updateMany', () => - this.runUpdateMany( - this.inputValidator.validateUpdateManyArgs(this.model, args) - ) - ) + .with('update', () => this.runUpdate(this.inputValidator.validateUpdateArgs(this.model, args))) + .with('updateMany', () => this.runUpdateMany(this.inputValidator.validateUpdateManyArgs(this.model, args))) .with('updateManyAndReturn', () => - this.runUpdateManyAndReturn( - this.inputValidator.validateUpdateManyAndReturnArgs( - this.model, - args - ) - ) - ) - .with('upsert', () => - this.runUpsert( - this.inputValidator.validateUpsertArgs(this.model, args) - ) + this.runUpdateManyAndReturn(this.inputValidator.validateUpdateManyAndReturnArgs(this.model, args)), ) + .with('upsert', () => this.runUpsert(this.inputValidator.validateUpsertArgs(this.model, args))) .exhaustive(); } private async runUpdate(args: UpdateArgs>) { const result = await this.safeTransaction(async (tx) => { - const updated = await this.update( - tx, - this.model, - args.where, - args.data - ); + const updated = await this.update(tx, this.model, args.where, args.data); return this.readUnique(tx, this.model, { select: args.select, include: args.include, omit: args.omit, - where: getIdValues( - this.schema, - this.model, - updated - ) as WhereInput, false>, + where: getIdValues(this.schema, this.model, updated) as WhereInput, false>, }); }); if (!result && this.hasPolicyEnabled) { - throw new RejectedByPolicyError( - this.model, - 'result is not allowed to be read back' - ); + throw new RejectedByPolicyError(this.model, 'result is not allowed to be read back'); } // NOTE: update can actually return null if the entity being updated is deleted @@ -78,43 +38,22 @@ export class UpdateOperationHandler< return result; } - private async runUpdateMany( - args: UpdateManyArgs> - ) { - return this.updateMany( - this.kysely, - this.model, - args.where, - args.data, - args.limit, - false - ); + private async runUpdateMany(args: UpdateManyArgs>) { + return this.updateMany(this.kysely, this.model, args.where, args.data, args.limit, false); } - private async runUpdateManyAndReturn( - args: UpdateManyAndReturnArgs> | undefined - ) { + private async runUpdateManyAndReturn(args: UpdateManyAndReturnArgs> | undefined) { if (!args) { return []; } return this.safeTransaction(async (tx) => { - const updateResult = await this.updateMany( - tx, - this.model, - args.where, - args.data, - args.limit, - true - ); + const updateResult = await this.updateMany(tx, this.model, args.where, args.data, args.limit, true); return this.read(tx, this.model, { select: args.select, omit: args.omit, where: { - OR: updateResult.map( - (item) => - getIdValues(this.schema, this.model, item) as any - ), + OR: updateResult.map((item) => getIdValues(this.schema, this.model, item) as any), } as any, // TODO: fix type }); }); @@ -122,15 +61,7 @@ export class UpdateOperationHandler< private async runUpsert(args: UpsertArgs>) { const result = await this.safeTransaction(async (tx) => { - let mutationResult = await this.update( - tx, - this.model, - args.where, - args.update, - undefined, - true, - false - ); + let mutationResult = await this.update(tx, this.model, args.where, args.update, undefined, true, false); if (!mutationResult) { // non-existing, create @@ -141,19 +72,16 @@ export class UpdateOperationHandler< select: args.select, include: args.include, omit: args.omit, - where: getIdValues( - this.schema, - this.model, - mutationResult - ) as WhereInput, false>, + where: getIdValues(this.schema, this.model, mutationResult) as WhereInput< + Schema, + GetModels, + false + >, }); }); if (!result && this.hasPolicyEnabled) { - throw new RejectedByPolicyError( - this.model, - 'result is not allowed to be read back' - ); + throw new RejectedByPolicyError(this.model, 'result is not allowed to be read back'); } return result; diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index 602946b2..cfda876d 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -2,13 +2,7 @@ import Decimal from 'decimal.js'; import stableStringify from 'json-stable-stringify'; import { match, P } from 'ts-pattern'; import { z, ZodType } from 'zod/v4'; -import type { - BuiltinType, - EnumDef, - FieldDef, - GetModels, - SchemaDef, -} from '../../schema'; +import type { BuiltinType, EnumDef, FieldDef, GetModels, SchemaDef } from '../../schema'; import { NUMERIC_FIELD_TYPES } from '../constants'; import { type AggregateArgs, @@ -26,19 +20,9 @@ import { type UpsertArgs, } from '../crud-types'; import { InternalError, QueryError } from '../errors'; -import { - fieldHasDefaultValue, - getEnum, - getModel, - getUniqueFields, - requireField, - requireModel, -} from '../query-utils'; - -type GetSchemaFunc = ( - model: GetModels, - options: Options -) => ZodType; +import { fieldHasDefaultValue, getEnum, getModel, getUniqueFields, requireField, requireModel } from '../query-utils'; + +type GetSchemaFunc = (model: GetModels, options: Options) => ZodType; export class InputValidator { private schemaCache = new Map(); @@ -46,15 +30,12 @@ export class InputValidator { constructor(private readonly schema: Schema) {} validateFindArgs(model: GetModels, unique: boolean, args: unknown) { - return this.validate< - FindArgs, true>, - Parameters[1] - >( + return this.validate, true>, Parameters[1]>( model, 'find', { unique, collection: true }, (model, options) => this.makeFindSchema(model, options), - args + args, ); } @@ -64,32 +45,27 @@ export class InputValidator { 'create', undefined, (model) => this.makeCreateSchema(model), - args + args, ); } validateCreateManyArgs(model: GetModels, args: unknown) { - return this.validate< - CreateManyArgs>, - undefined - >( + return this.validate>, undefined>( model, 'createMany', undefined, (model) => this.makeCreateManySchema(model), - args + args, ); } validateCreateManyAndReturnArgs(model: GetModels, args: unknown) { - return this.validate< - CreateManyAndReturnArgs> | undefined - >( + return this.validate> | undefined>( model, 'createManyAndReturn', undefined, (model) => this.makeCreateManyAndReturnSchema(model), - args + args, ); } @@ -99,7 +75,7 @@ export class InputValidator { 'update', undefined, (model) => this.makeUpdateSchema(model), - args + args, ); } @@ -109,19 +85,17 @@ export class InputValidator { 'updateMany', undefined, (model) => this.makeUpdateManySchema(model), - args + args, ); } validateUpdateManyAndReturnArgs(model: GetModels, args: unknown) { - return this.validate< - UpdateManyAndReturnArgs> | undefined - >( + return this.validate> | undefined>( model, 'updateManyAndReturn', undefined, (model) => this.makeUpdateManyAndReturnSchema(model), - args + args, ); } @@ -131,7 +105,7 @@ export class InputValidator { 'upsert', undefined, (model) => this.makeUpsertSchema(model), - args + args, ); } @@ -141,45 +115,37 @@ export class InputValidator { 'delete', undefined, (model) => this.makeDeleteSchema(model), - args + args, ); } validateDeleteManyArgs(model: GetModels, args: unknown) { - return this.validate< - DeleteManyArgs> | undefined - >( + return this.validate> | undefined>( model, 'deleteMany', undefined, (model) => this.makeDeleteManySchema(model), - args + args, ); } validateCountArgs(model: GetModels, args: unknown) { - return this.validate< - CountArgs> | undefined, - undefined - >( + return this.validate> | undefined, undefined>( model, 'count', undefined, (model) => this.makeCountSchema(model), - args + args, ); } validateAggregateArgs(model: GetModels, args: unknown) { - return this.validate< - AggregateArgs>, - undefined - >( + return this.validate>, undefined>( model, 'aggregate', undefined, (model) => this.makeAggregateSchema(model), - args + args, ); } @@ -189,7 +155,7 @@ export class InputValidator { 'groupBy', undefined, (model) => this.makeGroupBySchema(model), - args + args, ); } @@ -198,7 +164,7 @@ export class InputValidator { operation: string, options: Options, getSchema: GetSchemaFunc, - args: unknown + args: unknown, ) { const cacheKey = stableStringify({ model, @@ -219,10 +185,7 @@ export class InputValidator { // #region Find - private makeFindSchema( - model: string, - options: { unique: boolean; collection: boolean } - ) { + private makeFindSchema(model: string, options: { unique: boolean; collection: boolean }) { const fields: Record = {}; const where = this.makeWhereSchema(model, options.unique); if (options.unique) { @@ -240,10 +203,7 @@ export class InputValidator { if (options.collection) { fields['skip'] = z.number().int().nonnegative().optional(); fields['take'] = z.number().int().optional(); - fields['orderBy'] = this.orArray( - this.makeOrderBySchema(model, true, false), - true - ).optional(); + fields['orderBy'] = this.orArray(this.makeOrderBySchema(model, true, false), true).optional(); } let result: ZodType = z.object(fields).strict(); @@ -263,19 +223,13 @@ export class InputValidator { .with('Float', () => z.number()) .with('Boolean', () => z.boolean()) .with('BigInt', () => z.union([z.number(), z.bigint()])) - .with('Decimal', () => - z.union([z.number(), z.instanceof(Decimal), z.string()]) - ) + .with('Decimal', () => z.union([z.number(), z.instanceof(Decimal), z.string()])) .with('DateTime', () => z.union([z.date(), z.string().datetime()])) .with('Bytes', () => z.instanceof(Uint8Array)) .otherwise(() => z.unknown()); } - private makeWhereSchema( - model: string, - unique: boolean, - withoutRelationFields = false - ): ZodType { + private makeWhereSchema(model: string, unique: boolean, withoutRelationFields = false): ZodType { const modelDef = getModel(this.schema, model); if (!modelDef) { throw new QueryError(`Model "${model}" not found`); @@ -290,15 +244,10 @@ export class InputValidator { if (withoutRelationFields) { continue; } - fieldSchema = z.lazy(() => - this.makeWhereSchema(fieldDef.type, false).optional() - ); + fieldSchema = z.lazy(() => this.makeWhereSchema(fieldDef.type, false).optional()); // optional to-one relation allows null - fieldSchema = this.nullableIf( - fieldSchema, - !fieldDef.array && !!fieldDef.optional - ); + fieldSchema = this.nullableIf(fieldSchema, !fieldDef.array && !!fieldDef.optional); if (fieldDef.array) { // to-many relation @@ -325,22 +274,14 @@ export class InputValidator { if (enumDef) { // enum if (Object.keys(enumDef).length > 0) { - fieldSchema = this.makeEnumFilterSchema( - enumDef, - !!fieldDef.optional - ); + fieldSchema = this.makeEnumFilterSchema(enumDef, !!fieldDef.optional); } } else if (fieldDef.array) { // array field - fieldSchema = this.makeArrayFilterSchema( - fieldDef.type as BuiltinType - ); + fieldSchema = this.makeArrayFilterSchema(fieldDef.type as BuiltinType); } else { // primitive field - fieldSchema = this.makePrimitiveFilterSchema( - fieldDef.type as BuiltinType, - !!fieldDef.optional - ); + fieldSchema = this.makePrimitiveFilterSchema(fieldDef.type as BuiltinType, !!fieldDef.optional); } } @@ -357,16 +298,11 @@ export class InputValidator { fields[uniqueField.name] = z .object( Object.fromEntries( - Object.entries(uniqueField.defs).map( - ([key, def]) => [ - key, - this.makePrimitiveFilterSchema( - def.type as BuiltinType, - !!def.optional - ), - ] - ) - ) + Object.entries(uniqueField.defs).map(([key, def]) => [ + key, + this.makePrimitiveFilterSchema(def.type as BuiltinType, !!def.optional), + ]), + ), ) .optional(); } @@ -378,22 +314,16 @@ export class InputValidator { // logical operators fields['AND'] = this.orArray( - z.lazy(() => - this.makeWhereSchema(model, false, withoutRelationFields) - ), - true + z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), + true, ).optional(); fields['OR'] = z - .lazy(() => - this.makeWhereSchema(model, false, withoutRelationFields) - ) + .lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)) .array() .optional(); fields['NOT'] = this.orArray( - z.lazy(() => - this.makeWhereSchema(model, false, withoutRelationFields) - ), - true + z.lazy(() => this.makeWhereSchema(model, false, withoutRelationFields)), + true, ).optional(); const baseWhere = z.object(fields).strict(); @@ -403,9 +333,7 @@ export class InputValidator { // requires at least one unique field (field set) is required const uniqueFields = getUniqueFields(this.schema, model); if (uniqueFields.length === 0) { - throw new InternalError( - `Model "${model}" has no unique fields` - ); + throw new InternalError(`Model "${model}" has no unique fields`); } if (uniqueFields.length === 1) { @@ -416,9 +344,7 @@ export class InputValidator { } else { result = baseWhere.refine((value) => { // check that at least one unique field is set - return uniqueFields.some( - ({ name }) => value[name] !== undefined - ); + return uniqueFields.some(({ name }) => value[name] !== undefined); }, `At least one unique field or field set must be set`); } } @@ -427,13 +353,9 @@ export class InputValidator { } private makeEnumFilterSchema(enumDef: EnumDef, optional: boolean) { - const baseSchema = z.enum( - Object.keys(enumDef) as [string, ...string[]] - ); - const components = this.makeCommonPrimitiveFilterComponents( - baseSchema, - optional, - () => z.lazy(() => this.makeEnumFilterSchema(enumDef, optional)) + const baseSchema = z.enum(Object.keys(enumDef) as [string, ...string[]]); + const components = this.makeCommonPrimitiveFilterComponents(baseSchema, optional, () => + z.lazy(() => this.makeEnumFilterSchema(enumDef, optional)), ); return z.union([ this.nullableIf(baseSchema, optional), @@ -460,10 +382,7 @@ export class InputValidator { return match(type) .with('String', () => this.makeStringFilterSchema(optional)) .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.makeNumberFilterSchema( - this.makePrimitiveSchema(type), - optional - ) + this.makeNumberFilterSchema(this.makePrimitiveSchema(type), optional), ) .with('Boolean', () => this.makeBooleanFilterSchema(optional)) .with('DateTime', () => this.makeDateTimeFilterSchema(optional)) @@ -472,10 +391,8 @@ export class InputValidator { } private makeDateTimeFilterSchema(optional: boolean): ZodType { - return this.makeCommonPrimitiveFilterSchema( - z.union([z.string().datetime(), z.date()]), - optional, - () => z.lazy(() => this.makeDateTimeFilterSchema(optional)) + return this.makeCommonPrimitiveFilterSchema(z.union([z.string().datetime(), z.date()]), optional, () => + z.lazy(() => this.makeDateTimeFilterSchema(optional)), ); } @@ -484,19 +401,15 @@ export class InputValidator { this.nullableIf(z.boolean(), optional), z.object({ equals: this.nullableIf(z.boolean(), optional).optional(), - not: z - .lazy(() => this.makeBooleanFilterSchema(optional)) - .optional(), + not: z.lazy(() => this.makeBooleanFilterSchema(optional)).optional(), }), ]); } private makeBytesFilterSchema(optional: boolean): ZodType { const baseSchema = z.instanceof(Uint8Array); - const components = this.makeCommonPrimitiveFilterComponents( - baseSchema, - optional, - () => z.instanceof(Uint8Array) + const components = this.makeCommonPrimitiveFilterComponents(baseSchema, optional, () => + z.instanceof(Uint8Array), ); return z.union([ this.nullableIf(baseSchema, optional), @@ -509,11 +422,7 @@ export class InputValidator { ]); } - private makeCommonPrimitiveFilterComponents( - baseSchema: ZodType, - optional: boolean, - makeThis: () => ZodType - ) { + private makeCommonPrimitiveFilterComponents(baseSchema: ZodType, optional: boolean, makeThis: () => ZodType) { return { equals: this.nullableIf(baseSchema.optional(), optional), notEquals: this.nullableIf(baseSchema.optional(), optional), @@ -527,35 +436,22 @@ export class InputValidator { }; } - private makeCommonPrimitiveFilterSchema( - baseSchema: ZodType, - optional: boolean, - makeThis: () => ZodType - ) { + private makeCommonPrimitiveFilterSchema(baseSchema: ZodType, optional: boolean, makeThis: () => ZodType) { return z.union([ this.nullableIf(baseSchema, optional), - z.object( - this.makeCommonPrimitiveFilterComponents( - baseSchema, - optional, - makeThis - ) - ), + z.object(this.makeCommonPrimitiveFilterComponents(baseSchema, optional, makeThis)), ]); } - private makeNumberFilterSchema( - baseSchema: ZodType, - optional: boolean - ): ZodType { + private makeNumberFilterSchema(baseSchema: ZodType, optional: boolean): ZodType { return this.makeCommonPrimitiveFilterSchema(baseSchema, optional, () => - z.lazy(() => this.makeNumberFilterSchema(baseSchema, optional)) + z.lazy(() => this.makeNumberFilterSchema(baseSchema, optional)), ); } private makeStringFilterSchema(optional: boolean): ZodType { return this.makeCommonPrimitiveFilterSchema(z.string(), optional, () => - z.lazy(() => this.makeStringFilterSchema(optional)) + z.lazy(() => this.makeStringFilterSchema(optional)), ); } @@ -569,16 +465,8 @@ export class InputValidator { .union([ z.literal(true), z.object({ - select: z - .lazy(() => - this.makeSelectSchema(fieldDef.type) - ) - .optional(), - include: z - .lazy(() => - this.makeIncludeSchema(fieldDef.type) - ) - .optional(), + select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), + include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), }), ]) .optional(); @@ -587,9 +475,7 @@ export class InputValidator { } } - const toManyRelations = Object.entries(modelDef.fields).filter( - ([, value]) => value.relation && value.array - ); + const toManyRelations = Object.entries(modelDef.fields).filter(([, value]) => value.relation && value.array); if (toManyRelations.length > 0) { fields['_count'] = z @@ -603,17 +489,13 @@ export class InputValidator { .union([ z.boolean(), z.object({ - where: this.makeWhereSchema( - fieldDef.type, - false, - false - ), + where: this.makeWhereSchema(fieldDef.type, false, false), }), ]) .optional(), }), - {} as Record - ) + {} as Record, + ), ), ]) .optional(); @@ -644,21 +526,9 @@ export class InputValidator { .union([ z.literal(true), z.object({ - select: z - .lazy(() => - this.makeSelectSchema(fieldDef.type) - ) - .optional(), - include: z - .lazy(() => - this.makeIncludeSchema(fieldDef.type) - ) - .optional(), - where: z - .lazy(() => - this.makeWhereSchema(fieldDef.type, false) - ) - .optional(), + select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), + include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), + where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), }), ]) .optional(); @@ -668,11 +538,7 @@ export class InputValidator { return z.object(fields).strict(); } - private makeOrderBySchema( - model: string, - withRelation: boolean, - WithAggregation: boolean - ) { + private makeOrderBySchema(model: string, withRelation: boolean, WithAggregation: boolean) { const modelDef = requireModel(this.schema, model); const fields: Record = {}; const sort = z.union([z.literal('asc'), z.literal('desc')]); @@ -682,11 +548,7 @@ export class InputValidator { // relations if (withRelation) { fields[field] = z.lazy(() => - this.makeOrderBySchema( - fieldDef.type, - withRelation, - WithAggregation - ).optional() + this.makeOrderBySchema(fieldDef.type, withRelation, WithAggregation).optional(), ); } } else { @@ -697,10 +559,7 @@ export class InputValidator { sort, z.object({ sort, - nulls: z.union([ - z.literal('first'), - z.literal('last'), - ]), + nulls: z.union([z.literal('first'), z.literal('last')]), }), ]) .optional(); @@ -712,17 +571,9 @@ export class InputValidator { // aggregations if (WithAggregation) { - const aggregationFields = [ - '_count', - '_avg', - '_sum', - '_min', - '_max', - ]; + const aggregationFields = ['_count', '_avg', '_sum', '_min', '_max']; for (const agg of aggregationFields) { - fields[agg] = z.lazy(() => - this.makeOrderBySchema(model, true, false).optional() - ); + fields[agg] = z.lazy(() => this.makeOrderBySchema(model, true, false).optional()); } } @@ -731,9 +582,7 @@ export class InputValidator { private makeDistinctSchema(model: string) { const modelDef = requireModel(this.schema, model); - const nonRelationFields = Object.keys(modelDef.fields).filter( - (field) => !modelDef.fields[field]?.relation - ); + const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); return this.orArray(z.enum(nonRelationFields as any), true); } @@ -768,7 +617,7 @@ export class InputValidator { z.object({ select: this.makeSelectSchema(model).optional(), omit: this.makeOmitSchema(model).optional(), - }) + }), ); return this.refineForSelectOmitMutuallyExclusive(result).optional(); } @@ -777,16 +626,14 @@ export class InputValidator { model: string, canBeArray: boolean, withoutFields: string[] = [], - withoutRelationFields = false + withoutRelationFields = false, ) { const regularAndFkFields: any = {}; const regularAndRelationFields: any = {}; const modelDef = requireModel(this.schema, model); const hasRelation = !withoutRelationFields && - Object.entries(modelDef.fields).some( - ([f, def]) => !withoutFields.includes(f) && def.relation - ); + Object.entries(modelDef.fields).some(([f, def]) => !withoutFields.includes(f) && def.relation); Object.keys(modelDef.fields).forEach((field) => { if (withoutFields.includes(field)) { @@ -805,22 +652,14 @@ export class InputValidator { const oppositeField = fieldDef.relation.opposite; if (oppositeField) { excludeFields.push(oppositeField); - const oppositeFieldDef = requireField( - this.schema, - fieldDef.type, - oppositeField - ); + const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); if (oppositeFieldDef.relation?.fields) { excludeFields.push(...oppositeFieldDef.relation.fields); } } let fieldSchema: ZodType = z.lazy(() => - this.makeRelationManipulationSchema( - fieldDef, - excludeFields, - 'create' - ) + this.makeRelationManipulationSchema(fieldDef, excludeFields, 'create'), ); if (fieldDef.optional || fieldDef.array) { @@ -832,9 +671,7 @@ export class InputValidator { if (fieldDef.relation.fields) { allFksOptional = fieldDef.relation.fields.every((f) => { const fkDef = requireField(this.schema, model, f); - return ( - fkDef.optional || fieldHasDefaultValue(fkDef) - ); + return fkDef.optional || fieldHasDefaultValue(fkDef); }); } if (allFksOptional) { @@ -848,9 +685,7 @@ export class InputValidator { } regularAndRelationFields[field] = fieldSchema; } else { - let fieldSchema: ZodType = this.makePrimitiveSchema( - fieldDef.type - ); + let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type); if (fieldDef.array) { fieldSchema = z @@ -879,88 +714,53 @@ export class InputValidator { }); if (!hasRelation) { - return this.orArray( - z.object(regularAndFkFields).strict(), - canBeArray - ); + return this.orArray(z.object(regularAndFkFields).strict(), canBeArray); } else { return z.union([ z.object(regularAndFkFields).strict(), z.object(regularAndRelationFields).strict(), - ...(canBeArray - ? [z.array(z.object(regularAndFkFields).strict())] - : []), - ...(canBeArray - ? [z.array(z.object(regularAndRelationFields).strict())] - : []), + ...(canBeArray ? [z.array(z.object(regularAndFkFields).strict())] : []), + ...(canBeArray ? [z.array(z.object(regularAndRelationFields).strict())] : []), ]); } } - private makeRelationManipulationSchema( - fieldDef: FieldDef, - withoutFields: string[], - mode: 'create' | 'update' - ) { + private makeRelationManipulationSchema(fieldDef: FieldDef, withoutFields: string[], mode: 'create' | 'update') { const fieldType = fieldDef.type; const array = !!fieldDef.array; const fields: Record = { - create: this.makeCreateDataSchema( - fieldDef.type, - !!fieldDef.array, - withoutFields - ).optional(), + create: this.makeCreateDataSchema(fieldDef.type, !!fieldDef.array, withoutFields).optional(), connect: this.makeConnectDataSchema(fieldType, array).optional(), - connectOrCreate: this.makeConnectOrCreateDataSchema( - fieldType, - array, - withoutFields - ).optional(), + connectOrCreate: this.makeConnectOrCreateDataSchema(fieldType, array, withoutFields).optional(), }; if (array) { - fields['createMany'] = this.makeCreateManyDataSchema( - fieldType, - withoutFields - ).optional(); + fields['createMany'] = this.makeCreateManyDataSchema(fieldType, withoutFields).optional(); } if (mode === 'update') { if (fieldDef.optional || fieldDef.array) { // disconnect and delete are only available for optional/to-many relations - fields['disconnect'] = this.makeDisconnectDataSchema( - fieldType, - array - ).optional(); + fields['disconnect'] = this.makeDisconnectDataSchema(fieldType, array).optional(); - fields['delete'] = this.makeDeleteRelationDataSchema( - fieldType, - array, - true - ).optional(); + fields['delete'] = this.makeDeleteRelationDataSchema(fieldType, array, true).optional(); } fields['update'] = array ? this.orArray( z.object({ where: this.makeWhereSchema(fieldType, true), - data: this.makeUpdateDataSchema( - fieldType, - withoutFields - ), + data: this.makeUpdateDataSchema(fieldType, withoutFields), }), - true + true, ).optional() : z .union([ z.object({ where: this.makeWhereSchema(fieldType, true), - data: this.makeUpdateDataSchema( - fieldType, - withoutFields - ), + data: this.makeUpdateDataSchema(fieldType, withoutFields), }), this.makeUpdateDataSchema(fieldType, withoutFields), ]) @@ -969,49 +769,32 @@ export class InputValidator { fields['upsert'] = this.orArray( z.object({ where: this.makeWhereSchema(fieldType, true), - create: this.makeCreateDataSchema( - fieldType, - false, - withoutFields - ), + create: this.makeCreateDataSchema(fieldType, false, withoutFields), update: this.makeUpdateDataSchema(fieldType, withoutFields), }), - true + true, ).optional(); if (array) { // to-many relation specifics - fields['set'] = this.makeSetDataSchema( - fieldType, - true - ).optional(); + fields['set'] = this.makeSetDataSchema(fieldType, true).optional(); fields['updateMany'] = this.orArray( z.object({ where: this.makeWhereSchema(fieldType, false, true), - data: this.makeUpdateDataSchema( - fieldType, - withoutFields - ), + data: this.makeUpdateDataSchema(fieldType, withoutFields), }), - true - ).optional(); - - fields['deleteMany'] = this.makeDeleteRelationDataSchema( - fieldType, true, - false ).optional(); + + fields['deleteMany'] = this.makeDeleteRelationDataSchema(fieldType, true, false).optional(); } } return z .object(fields) .strict() - .refine( - (v) => Object.keys(v).length > 0, - 'At least one action is required' - ); + .refine((v) => Object.keys(v).length > 0, 'At least one action is required'); } private makeSetDataSchema(model: string, canBeArray: boolean) { @@ -1033,27 +816,15 @@ export class InputValidator { } } - private makeDeleteRelationDataSchema( - model: string, - toManyRelation: boolean, - uniqueFilter: boolean - ) { + private makeDeleteRelationDataSchema(model: string, toManyRelation: boolean, uniqueFilter: boolean) { return toManyRelation ? this.orArray(this.makeWhereSchema(model, uniqueFilter), true) : z.union([z.boolean(), this.makeWhereSchema(model, uniqueFilter)]); } - private makeConnectOrCreateDataSchema( - model: string, - canBeArray: boolean, - withoutFields: string[] - ) { + private makeConnectOrCreateDataSchema(model: string, canBeArray: boolean, withoutFields: string[]) { const whereSchema = this.makeWhereSchema(model, true); - const createSchema = this.makeCreateDataSchema( - model, - false, - withoutFields - ); + const createSchema = this.makeCreateDataSchema(model, false, withoutFields); return this.orArray( z .object({ @@ -1061,19 +832,14 @@ export class InputValidator { create: createSchema, }) .strict(), - canBeArray + canBeArray, ); } private makeCreateManyDataSchema(model: string, withoutFields: string[]) { return z .object({ - data: this.makeCreateDataSchema( - model, - true, - withoutFields, - true - ), + data: this.makeCreateDataSchema(model, true, withoutFields, true), skipDuplicates: z.boolean().optional(), }) .strict(); @@ -1113,7 +879,7 @@ export class InputValidator { z.object({ select: this.makeSelectSchema(model).optional(), omit: this.makeOmitSchema(model).optional(), - }) + }), ); return this.refineForSelectOmitMutuallyExclusive(result); } @@ -1133,16 +899,12 @@ export class InputValidator { return this.refineForSelectIncludeMutuallyExclusive(schema); } - private makeUpdateDataSchema( - model: string, - withoutFields: string[] = [], - withoutRelationFields = false - ) { + private makeUpdateDataSchema(model: string, withoutFields: string[] = [], withoutRelationFields = false) { const regularAndFkFields: any = {}; const regularAndRelationFields: any = {}; const modelDef = requireModel(this.schema, model); const hasRelation = Object.entries(modelDef.fields).some( - ([key, value]) => value.relation && !withoutFields.includes(key) + ([key, value]) => value.relation && !withoutFields.includes(key), ); Object.keys(modelDef.fields).forEach((field) => { @@ -1159,23 +921,13 @@ export class InputValidator { const oppositeField = fieldDef.relation.opposite; if (oppositeField) { excludeFields.push(oppositeField); - const oppositeFieldDef = requireField( - this.schema, - fieldDef.type, - oppositeField - ); + const oppositeFieldDef = requireField(this.schema, fieldDef.type, oppositeField); if (oppositeFieldDef.relation?.fields) { excludeFields.push(...oppositeFieldDef.relation.fields); } } let fieldSchema: ZodType = z - .lazy(() => - this.makeRelationManipulationSchema( - fieldDef, - excludeFields, - 'update' - ) - ) + .lazy(() => this.makeRelationManipulationSchema(fieldDef, excludeFields, 'update')) .optional(); // optional to-one relation can be null if (fieldDef.optional && !fieldDef.array) { @@ -1183,19 +935,14 @@ export class InputValidator { } regularAndRelationFields[field] = fieldSchema; } else { - let fieldSchema: ZodType = this.makePrimitiveSchema( - fieldDef.type - ).optional(); + let fieldSchema: ZodType = this.makePrimitiveSchema(fieldDef.type).optional(); if (this.isNumericField(fieldDef)) { fieldSchema = z.union([ fieldSchema, z .object({ - set: this.nullableIf( - z.number().optional(), - !!fieldDef.optional - ), + set: this.nullableIf(z.number().optional(), !!fieldDef.optional), increment: z.number().optional(), decrement: z.number().optional(), multiply: z.number().optional(), @@ -1203,7 +950,7 @@ export class InputValidator { }) .refine( (v) => Object.keys(v).length === 1, - 'Only one of "set", "increment", "decrement", "multiply", or "divide" can be provided' + 'Only one of "set", "increment", "decrement", "multiply", or "divide" can be provided', ), ]); } @@ -1215,14 +962,11 @@ export class InputValidator { z .object({ set: z.array(fieldSchema).optional(), - push: this.orArray( - fieldSchema, - true - ).optional(), + push: this.orArray(fieldSchema, true).optional(), }) .refine( (v) => Object.keys(v).length === 1, - 'Only one of "set", "push" can be provided' + 'Only one of "set", "push" can be provided', ), ]) .optional(); @@ -1242,10 +986,7 @@ export class InputValidator { if (!hasRelation) { return z.object(regularAndFkFields).strict(); } else { - return z.union([ - z.object(regularAndFkFields).strict(), - z.object(regularAndRelationFields).strict(), - ]); + return z.union([z.object(regularAndFkFields).strict(), z.object(regularAndRelationFields).strict()]); } } @@ -1284,10 +1025,7 @@ export class InputValidator { where: this.makeWhereSchema(model, false).optional(), skip: z.number().int().nonnegative().optional(), take: z.number().int().optional(), - orderBy: this.orArray( - this.makeOrderBySchema(model, true, false), - true - ).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), select: this.makeCountAggregateInputSchema(model).optional(), }) .strict() @@ -1301,10 +1039,13 @@ export class InputValidator { z .object({ _all: z.literal(true).optional(), - ...Object.keys(modelDef.fields).reduce((acc, field) => { - acc[field] = z.literal(true).optional(); - return acc; - }, {} as Record), + ...Object.keys(modelDef.fields).reduce( + (acc, field) => { + acc[field] = z.literal(true).optional(); + return acc; + }, + {} as Record, + ), }) .strict(), ]); @@ -1320,10 +1061,7 @@ export class InputValidator { where: this.makeWhereSchema(model, false).optional(), skip: z.number().int().nonnegative().optional(), take: z.number().int().optional(), - orderBy: this.orArray( - this.makeOrderBySchema(model, true, false), - true - ).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), _count: this.makeCountAggregateInputSchema(model).optional(), _avg: this.makeSumAvgInputSchema(model).optional(), _sum: this.makeSumAvgInputSchema(model).optional(), @@ -1337,42 +1075,43 @@ export class InputValidator { makeSumAvgInputSchema(model: GetModels) { const modelDef = requireModel(this.schema, model); return z.object( - Object.keys(modelDef.fields).reduce((acc, field) => { - const fieldDef = requireField(this.schema, model, field); - if (this.isNumericField(fieldDef)) { - acc[field] = z.literal(true).optional(); - } - return acc; - }, {} as Record) + Object.keys(modelDef.fields).reduce( + (acc, field) => { + const fieldDef = requireField(this.schema, model, field); + if (this.isNumericField(fieldDef)) { + acc[field] = z.literal(true).optional(); + } + return acc; + }, + {} as Record, + ), ); } makeMinMaxInputSchema(model: GetModels) { const modelDef = requireModel(this.schema, model); return z.object( - Object.keys(modelDef.fields).reduce((acc, field) => { - const fieldDef = requireField(this.schema, model, field); - if (!fieldDef.relation && !fieldDef.array) { - acc[field] = z.literal(true).optional(); - } - return acc; - }, {} as Record) + Object.keys(modelDef.fields).reduce( + (acc, field) => { + const fieldDef = requireField(this.schema, model, field); + if (!fieldDef.relation && !fieldDef.array) { + acc[field] = z.literal(true).optional(); + } + return acc; + }, + {} as Record, + ), ); } private makeGroupBySchema(model: GetModels) { const modelDef = requireModel(this.schema, model); - const nonRelationFields = Object.keys(modelDef.fields).filter( - (field) => !modelDef.fields[field]?.relation - ); + const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); let schema = z .object({ where: this.makeWhereSchema(model, false).optional(), - orderBy: this.orArray( - this.makeOrderBySchema(model, false, true), - true - ).optional(), + orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), by: this.orArray(z.enum(nonRelationFields), true), having: this.makeWhereSchema(model, false, true).optional(), skip: z.number().int().nonnegative().optional(), @@ -1423,14 +1162,14 @@ export class InputValidator { private refineForSelectIncludeMutuallyExclusive(schema: ZodType) { return schema.refine( (value: any) => !(value['select'] && value['include']), - '"select" and "include" cannot be used together' + '"select" and "include" cannot be used together', ); } private refineForSelectOmitMutuallyExclusive(schema: ZodType) { return schema.refine( (value: any) => !(value['select'] && value['omit']), - '"select" and "omit" cannot be used together' + '"select" and "omit" cannot be used together', ); } diff --git a/packages/runtime/src/client/executor/name-mapper.ts b/packages/runtime/src/client/executor/name-mapper.ts index efde7582..d5a893d5 100644 --- a/packages/runtime/src/client/executor/name-mapper.ts +++ b/packages/runtime/src/client/executor/name-mapper.ts @@ -32,15 +32,10 @@ export class QueryNameMapper extends OperationNodeTransformer { this.modelToTableMap.set(modelName, mappedName); } - for (const [fieldName, fieldDef] of Object.entries( - modelDef.fields - )) { + for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { const mappedName = this.getMappedName(fieldDef); if (mappedName) { - this.fieldToColumnMap.set( - `${modelName}.${fieldName}`, - mappedName - ); + this.fieldToColumnMap.set(`${modelName}.${fieldName}`, mappedName); } } } @@ -73,9 +68,7 @@ export class QueryNameMapper extends OperationNodeTransformer { } protected override transformReturning(node: ReturningNode) { - return ReturningNode.create( - this.transformSelections(node.selections, node) - ); + return ReturningNode.create(this.transformSelections(node.selections, node)); } protected override transformUpdateQuery(node: UpdateQueryNode) { @@ -93,15 +86,9 @@ export class QueryNameMapper extends OperationNodeTransformer { } } - protected override transformDeleteQuery( - node: DeleteQueryNode - ): DeleteQueryNode { + protected override transformDeleteQuery(node: DeleteQueryNode): DeleteQueryNode { let pushed = false; - if ( - node.from?.froms && - node.from.froms.length === 1 && - node.from.froms[0] - ) { + if (node.from?.froms && node.from.froms.length === 1 && node.from.froms[0]) { const from = node.from.froms[0]; if (TableNode.is(from)) { this.modelStack.push(from.table.identifier.name); @@ -126,9 +113,7 @@ export class QueryNameMapper extends OperationNodeTransformer { } if (node.from.froms.length > 1) { - throw new InternalError( - `SelectQueryNode must have a single table in from clause` - ); + throw new InternalError(`SelectQueryNode must have a single table in from clause`); } let pushed = false; @@ -141,9 +126,7 @@ export class QueryNameMapper extends OperationNodeTransformer { pushed = true; } - const selections = node.selections - ? this.transformSelections(node.selections, node) - : node.selections; + const selections = node.selections ? this.transformSelections(node.selections, node) : node.selections; try { return { @@ -157,10 +140,7 @@ export class QueryNameMapper extends OperationNodeTransformer { } } - private transformSelections( - selections: readonly SelectionNode[], - contextNode: OperationNode - ) { + private transformSelections(selections: readonly SelectionNode[], contextNode: OperationNode) { const result: SelectionNode[] = []; for (const selection of selections) { @@ -170,13 +150,8 @@ export class QueryNameMapper extends OperationNodeTransformer { if (SelectAllNode.is(selection.selection)) { selectAllFromModel = this.currentModel; isSelectAll = true; - } else if ( - ReferenceNode.is(selection.selection) && - SelectAllNode.is(selection.selection.column) - ) { - selectAllFromModel = - selection.selection.table?.table.identifier.name ?? - this.currentModel; + } else if (ReferenceNode.is(selection.selection) && SelectAllNode.is(selection.selection.column)) { + selectAllFromModel = selection.selection.table?.table.identifier.name ?? this.currentModel; isSelectAll = true; } @@ -184,31 +159,21 @@ export class QueryNameMapper extends OperationNodeTransformer { if (!selectAllFromModel) { continue; } else { - const scalarFields = this.getModelScalarFields( - contextNode, - selectAllFromModel - ); - const fromModelDef = requireModel( - this.schema, - selectAllFromModel - ); - const mappedTableName = - this.getMappedName(fromModelDef) ?? selectAllFromModel; + const scalarFields = this.getModelScalarFields(contextNode, selectAllFromModel); + const fromModelDef = requireModel(this.schema, selectAllFromModel); + const mappedTableName = this.getMappedName(fromModelDef) ?? selectAllFromModel; result.push( ...scalarFields.map((fieldName) => { const fieldRef = ReferenceNode.create( ColumnNode.create(this.mapFieldName(fieldName)), - TableNode.create(mappedTableName) + TableNode.create(mappedTableName), ); return SelectionNode.create( this.fieldHasMappedName(fieldName) - ? AliasNode.create( - fieldRef, - IdentifierNode.create(fieldName) - ) - : fieldRef + ? AliasNode.create(fieldRef, IdentifierNode.create(fieldName)) + : fieldRef, ); - }) + }), ); } } else { @@ -220,29 +185,22 @@ export class QueryNameMapper extends OperationNodeTransformer { } private transformSelectionWithAlias(node: SelectionNode) { - if ( - ColumnNode.is(node.selection) && - this.fieldHasMappedName(node.selection.column.name) - ) { + if (ColumnNode.is(node.selection) && this.fieldHasMappedName(node.selection.column.name)) { return SelectionNode.create( AliasNode.create( this.transformColumn(node.selection), - IdentifierNode.create(node.selection.column.name) - ) + IdentifierNode.create(node.selection.column.name), + ), ); } else if ( ReferenceNode.is(node.selection) && - this.fieldHasMappedName( - (node.selection.column as ColumnNode).column.name - ) + this.fieldHasMappedName((node.selection.column as ColumnNode).column.name) ) { return SelectionNode.create( AliasNode.create( this.transformReference(node.selection), - IdentifierNode.create( - (node.selection.column as ColumnNode).column.name - ) - ) + IdentifierNode.create((node.selection.column as ColumnNode).column.name), + ), ); } else { return this.transformSelection(node); @@ -272,9 +230,7 @@ export class QueryNameMapper extends OperationNodeTransformer { } private getMappedName(def: ModelDef | FieldDef) { - const mapAttr = def.attributes?.find( - (attr) => attr.name === '@@map' || attr.name === '@map' - ); + const mapAttr = def.attributes?.find((attr) => attr.name === '@@map' || attr.name === '@map'); if (mapAttr) { const nameArg = mapAttr.args?.find((arg) => arg.name === 'name'); if (nameArg && nameArg.value.kind === 'literal') { @@ -288,9 +244,7 @@ export class QueryNameMapper extends OperationNodeTransformer { if (!this.currentModel) { return fieldName; } - const mappedName = this.fieldToColumnMap.get( - `${this.currentModel}.${fieldName}` - ); + const mappedName = this.fieldToColumnMap.get(`${this.currentModel}.${fieldName}`); if (mappedName) { return mappedName; } else { @@ -304,10 +258,7 @@ export class QueryNameMapper extends OperationNodeTransformer { } } - private getModelScalarFields( - contextNode: OperationNode, - model: string | undefined - ) { + private getModelScalarFields(contextNode: OperationNode, model: string | undefined) { this.requireCurrentModel(contextNode); model = model ?? this.currentModel; const modelDef = requireModel(this.schema, model!); diff --git a/packages/runtime/src/client/executor/zenstack-driver.ts b/packages/runtime/src/client/executor/zenstack-driver.ts index cc60596c..651c3eaf 100644 --- a/packages/runtime/src/client/executor/zenstack-driver.ts +++ b/packages/runtime/src/client/executor/zenstack-driver.ts @@ -1,11 +1,4 @@ -import type { - CompiledQuery, - DatabaseConnection, - Driver, - Log, - QueryResult, - TransactionSettings, -} from 'kysely'; +import type { CompiledQuery, DatabaseConnection, Driver, Log, QueryResult, TransactionSettings } from 'kysely'; /** * Copied from kysely's RuntimeDriver @@ -72,14 +65,8 @@ export class ZenStackDriver implements Driver { await this.#driver.releaseConnection(connection); } - async beginTransaction( - connection: DatabaseConnection, - settings: TransactionSettings - ): Promise { - const result = await this.#driver.beginTransaction( - connection, - settings - ); + async beginTransaction(connection: DatabaseConnection, settings: TransactionSettings): Promise { + const result = await this.#driver.beginTransaction(connection, settings); this.txConnection = connection; return result; } @@ -118,10 +105,7 @@ export class ZenStackDriver implements Driver { } #needsLogging(): boolean { - return ( - this.#log.isLevelEnabled('query') || - this.#log.isLevelEnabled('error') - ); + return this.#log.isLevelEnabled('query') || this.#log.isLevelEnabled('error'); } // This method monkey patches the database connection's executeQuery method @@ -133,9 +117,7 @@ export class ZenStackDriver implements Driver { // eslint-disable-next-line @typescript-eslint/no-this-alias const dis = this; - connection.executeQuery = async ( - compiledQuery - ): Promise> => { + connection.executeQuery = async (compiledQuery): Promise> => { let caughtError: unknown; const startTime = performanceNow(); @@ -152,19 +134,12 @@ export class ZenStackDriver implements Driver { } }; - connection.streamQuery = async function* ( - compiledQuery, - chunkSize - ): AsyncIterableIterator> { + connection.streamQuery = async function* (compiledQuery, chunkSize): AsyncIterableIterator> { let caughtError: unknown; const startTime = performanceNow(); try { - for await (const result of streamQuery.call( - connection, - compiledQuery, - chunkSize - )) { + for await (const result of streamQuery.call(connection, compiledQuery, chunkSize)) { yield result; } } catch (error) { @@ -179,11 +154,7 @@ export class ZenStackDriver implements Driver { }; } - async #logError( - error: unknown, - compiledQuery: CompiledQuery, - startTime: number - ): Promise { + async #logError(error: unknown, compiledQuery: CompiledQuery, startTime: number): Promise { await this.#log.error(() => ({ level: 'error', error, @@ -192,11 +163,7 @@ export class ZenStackDriver implements Driver { })); } - async #logQuery( - compiledQuery: CompiledQuery, - startTime: number, - isStream = false - ): Promise { + async #logQuery(compiledQuery: CompiledQuery, startTime: number, isStream = false): Promise { await this.#log.query(() => ({ level: 'query', isStream, @@ -211,10 +178,7 @@ export class ZenStackDriver implements Driver { } export function performanceNow() { - if ( - typeof performance !== 'undefined' && - typeof performance.now === 'function' - ) { + if (typeof performance !== 'undefined' && typeof performance.now === 'function') { return performance.now(); } else { return Date.now(); diff --git a/packages/runtime/src/client/executor/zenstack-query-executor.ts b/packages/runtime/src/client/executor/zenstack-query-executor.ts index b0c38bbc..3803f3f6 100644 --- a/packages/runtime/src/client/executor/zenstack-query-executor.ts +++ b/packages/runtime/src/client/executor/zenstack-query-executor.ts @@ -27,18 +27,13 @@ import type { GetModels, SchemaDef } from '../../schema'; import type { ClientImpl } from '../client-impl'; import type { ClientContract } from '../contract'; import { InternalError, QueryError } from '../errors'; -import type { - MutationInterceptionFilterResult, - OnKyselyQueryTransactionCallback, -} from '../plugin'; +import type { MutationInterceptionFilterResult, OnKyselyQueryTransactionCallback } from '../plugin'; import { QueryNameMapper } from './name-mapper'; import type { ZenStackDriver } from './zenstack-driver'; type QueryId = { queryId: string }; -export class ZenStackQueryExecutor< - Schema extends SchemaDef -> extends DefaultQueryExecutor { +export class ZenStackQueryExecutor extends DefaultQueryExecutor { private readonly nameMapper: QueryNameMapper; constructor( @@ -47,7 +42,7 @@ export class ZenStackQueryExecutor< private readonly compiler: QueryCompiler, adapter: DialectAdapter, private readonly connectionProvider: ConnectionProvider, - plugins: KyselyPlugin[] = [] + plugins: KyselyPlugin[] = [], ) { super(compiler, adapter, connectionProvider, plugins); this.nameMapper = new QueryNameMapper(client.$schema); @@ -61,55 +56,36 @@ export class ZenStackQueryExecutor< return this.client.$options; } - override async executeQuery( - compiledQuery: CompiledQuery, - queryId: QueryId - ) { + override async executeQuery(compiledQuery: CompiledQuery, queryId: QueryId) { let queryNode = compiledQuery.query; - let mutationInterceptionInfo: PromiseType< - ReturnType - >; + let mutationInterceptionInfo: PromiseType>; if (this.isMutationNode(queryNode) && this.hasMutationHooks) { - mutationInterceptionInfo = - await this.callMutationInterceptionFilters(queryNode); + mutationInterceptionInfo = await this.callMutationInterceptionFilters(queryNode); } const task = async () => { // call before mutation hooks - await this.callBeforeMutationHooks( - queryNode, - mutationInterceptionInfo - ); + await this.callBeforeMutationHooks(queryNode, mutationInterceptionInfo); // TODO: make sure insert and delete return rows const oldQueryNode = queryNode; if ( - (InsertQueryNode.is(queryNode) || - DeleteQueryNode.is(queryNode)) && + (InsertQueryNode.is(queryNode) || DeleteQueryNode.is(queryNode)) && mutationInterceptionInfo?.loadAfterMutationEntity ) { // need to make sure the query node has "returnAll" // for insert and delete queries queryNode = { ...queryNode, - returning: ReturningNode.create([ - SelectionNode.createSelectAll(), - ]), + returning: ReturningNode.create([SelectionNode.createSelectAll()]), }; } // proceed with the query with kysely interceptors - const result = await this.proceedQueryWithKyselyInterceptors( - queryNode, - queryId - ); + const result = await this.proceedQueryWithKyselyInterceptors(queryNode, queryId); // call after mutation hooks - await this.callAfterQueryInterceptionFilters( - result, - queryNode, - mutationInterceptionInfo - ); + await this.callAfterQueryInterceptionFilters(result, queryNode, mutationInterceptionInfo); if (oldQueryNode !== queryNode) { // TODO: trim the result to the original query node @@ -118,23 +94,15 @@ export class ZenStackQueryExecutor< return result; }; - return this.executeWithTransaction( - task, - !!mutationInterceptionInfo?.useTransactionForMutation - ); + return this.executeWithTransaction(task, !!mutationInterceptionInfo?.useTransactionForMutation); } - private proceedQueryWithKyselyInterceptors( - queryNode: RootOperationNode, - queryId: QueryId - ) { + private proceedQueryWithKyselyInterceptors(queryNode: RootOperationNode, queryId: QueryId) { let proceed = (q: RootOperationNode) => this.proceedQuery(q, queryId); - const makeTx = - (p: typeof proceed) => - (callback: OnKyselyQueryTransactionCallback) => { - return this.executeWithTransaction(() => callback(p)); - }; + const makeTx = (p: typeof proceed) => (callback: OnKyselyQueryTransactionCallback) => { + return this.executeWithTransaction(() => callback(p)); + }; const hooks = this.options.plugins @@ -165,24 +133,18 @@ export class ZenStackQueryExecutor< try { return this.driver.txConnection ? await super - .withConnectionProvider( - new SingleConnectionProvider(this.driver.txConnection) - ) + .withConnectionProvider(new SingleConnectionProvider(this.driver.txConnection)) .executeQuery(compiled, queryId) : await super.executeQuery(compiled, queryId); } catch (err) { throw new QueryError( - `Failed to execute query: ${err}, sql: ${compiled.sql}, parameters: ${compiled.parameters}` + `Failed to execute query: ${err}, sql: ${compiled.sql}, parameters: ${compiled.parameters}`, ); } } private isMutationNode(queryNode: RootOperationNode) { - return ( - InsertQueryNode.is(queryNode) || - UpdateQueryNode.is(queryNode) || - DeleteQueryNode.is(queryNode) - ); + return InsertQueryNode.is(queryNode) || UpdateQueryNode.is(queryNode) || DeleteQueryNode.is(queryNode); } override withPlugin(plugin: KyselyPlugin) { @@ -192,7 +154,7 @@ export class ZenStackQueryExecutor< this.compiler, this.adapter, this.connectionProvider, - [...this.plugins, plugin] + [...this.plugins, plugin], ); } @@ -203,7 +165,7 @@ export class ZenStackQueryExecutor< this.compiler, this.adapter, this.connectionProvider, - [...this.plugins, ...plugins] + [...this.plugins, ...plugins], ); } @@ -214,7 +176,7 @@ export class ZenStackQueryExecutor< this.compiler, this.adapter, this.connectionProvider, - [plugin, ...this.plugins] + [plugin, ...this.plugins], ); } override withoutPlugins() { @@ -224,24 +186,15 @@ export class ZenStackQueryExecutor< this.compiler, this.adapter, this.connectionProvider, - [] + [], ); } override withConnectionProvider(connectionProvider: ConnectionProvider) { - return new ZenStackQueryExecutor( - this.client, - this.driver, - this.compiler, - this.adapter, - connectionProvider - ); + return new ZenStackQueryExecutor(this.client, this.driver, this.compiler, this.adapter, connectionProvider); } - private async executeWithTransaction( - callback: () => Promise, - useTransaction = true - ) { + private async executeWithTransaction(callback: () => Promise, useTransaction = true) { if (!useTransaction || this.driver.txConnection) { return callback(); } else { @@ -261,26 +214,17 @@ export class ZenStackQueryExecutor< private get hasMutationHooks() { return this.client.$options.plugins?.some( - (plugin) => - plugin.beforeEntityMutation || plugin.afterEntityMutation + (plugin) => plugin.beforeEntityMutation || plugin.afterEntityMutation, ); } private getMutationModel(queryNode: OperationNode): GetModels { return match(queryNode) - .when( - InsertQueryNode.is, - (node) => node.into!.table.identifier.name - ) - .when( - UpdateQueryNode.is, - (node) => (node.table as TableNode).table.identifier.name - ) + .when(InsertQueryNode.is, (node) => node.into!.table.identifier.name) + .when(UpdateQueryNode.is, (node) => (node.table as TableNode).table.identifier.name) .when(DeleteQueryNode.is, (node) => { if (node.from.froms.length !== 1) { - throw new InternalError( - `Delete query must have exactly one from table` - ); + throw new InternalError(`Delete query must have exactly one from table`); } return (node.from.froms[0] as TableNode).table.identifier.name; }) @@ -289,9 +233,7 @@ export class ZenStackQueryExecutor< }) as GetModels; } - private async callMutationInterceptionFilters( - queryNode: UpdateQueryNode | InsertQueryNode | DeleteQueryNode - ) { + private async callMutationInterceptionFilters(queryNode: UpdateQueryNode | InsertQueryNode | DeleteQueryNode) { const plugins = this.client.$options.plugins; if (plugins) { const mutationModel = this.getMutationModel(queryNode); @@ -318,32 +260,21 @@ export class ZenStackQueryExecutor< if (!plugin.mutationInterceptionFilter) { result.intercept = true; } else { - const filterResult = - await plugin.mutationInterceptionFilter({ - model: mutationModel, - action, - queryNode, - }); + const filterResult = await plugin.mutationInterceptionFilter({ + model: mutationModel, + action, + queryNode, + }); result.intercept ||= filterResult.intercept; - result.useTransactionForMutation ||= - filterResult.useTransactionForMutation; - result.loadBeforeMutationEntity ||= - filterResult.loadBeforeMutationEntity; - result.loadAfterMutationEntity ||= - filterResult.loadAfterMutationEntity; + result.useTransactionForMutation ||= filterResult.useTransactionForMutation; + result.loadBeforeMutationEntity ||= filterResult.loadBeforeMutationEntity; + result.loadAfterMutationEntity ||= filterResult.loadAfterMutationEntity; } } let beforeMutationEntities: Record[] | undefined; - if ( - result.loadBeforeMutationEntity && - (UpdateQueryNode.is(queryNode) || DeleteQueryNode.is(queryNode)) - ) { - beforeMutationEntities = await this.loadEntities( - this.kysely, - mutationModel, - where - ); + if (result.loadBeforeMutationEntity && (UpdateQueryNode.is(queryNode) || DeleteQueryNode.is(queryNode))) { + beforeMutationEntities = await this.loadEntities(this.kysely, mutationModel, where); } return { @@ -360,9 +291,7 @@ export class ZenStackQueryExecutor< private callBeforeMutationHooks( queryNode: OperationNode, - mutationInterceptionInfo: PromiseType< - ReturnType - > + mutationInterceptionInfo: PromiseType>, ) { if (!mutationInterceptionInfo?.intercept) { return; @@ -376,8 +305,7 @@ export class ZenStackQueryExecutor< model: this.getMutationModel(queryNode), action: mutationInterceptionInfo.action, queryNode, - entities: - mutationInterceptionInfo.beforeMutationEntities, + entities: mutationInterceptionInfo.beforeMutationEntities, }); } } @@ -387,9 +315,7 @@ export class ZenStackQueryExecutor< private async callAfterQueryInterceptionFilters( queryResult: QueryResult, queryNode: OperationNode, - mutationInterceptionInfo: PromiseType< - ReturnType - > + mutationInterceptionInfo: PromiseType>, ) { if (!mutationInterceptionInfo?.intercept) { return; @@ -399,21 +325,16 @@ export class ZenStackQueryExecutor< const mutationModel = this.getMutationModel(queryNode); for (const plugin of this.options.plugins) { if (plugin.afterEntityMutation) { - let afterMutationEntities: - | Record[] - | undefined = undefined; + let afterMutationEntities: Record[] | undefined = undefined; if (mutationInterceptionInfo.loadAfterMutationEntity) { if (UpdateQueryNode.is(queryNode)) { afterMutationEntities = await this.loadEntities( this.kysely, mutationModel, - mutationInterceptionInfo.where + mutationInterceptionInfo.where, ); } else { - afterMutationEntities = queryResult.rows as Record< - string, - unknown - >[]; + afterMutationEntities = queryResult.rows as Record[]; } } @@ -421,8 +342,7 @@ export class ZenStackQueryExecutor< model: this.getMutationModel(queryNode), action: mutationInterceptionInfo.action, queryNode, - beforeMutationEntities: - mutationInterceptionInfo.beforeMutationEntities, + beforeMutationEntities: mutationInterceptionInfo.beforeMutationEntities, afterMutationEntities, }); } @@ -433,7 +353,7 @@ export class ZenStackQueryExecutor< private async loadEntities( kysely: Kysely, model: GetModels, - where: WhereNode | undefined + where: WhereNode | undefined, ): Promise[]> { const selectQuery = kysely.selectFrom(model).selectAll(); let selectQueryNode = selectQuery.toOperationNode() as SelectQueryNode; @@ -441,17 +361,12 @@ export class ZenStackQueryExecutor< ...selectQueryNode, where: this.andNodes(selectQueryNode.where, where), }; - const compiled = kysely - .getExecutor() - .compileQuery(selectQueryNode, { queryId: `zenstack-${nanoid()}` }); + const compiled = kysely.getExecutor().compileQuery(selectQueryNode, { queryId: `zenstack-${nanoid()}` }); const result = await kysely.executeQuery(compiled); return result.rows as Record[]; } - private andNodes( - condition1: WhereNode | undefined, - condition2: WhereNode | undefined - ) { + private andNodes(condition1: WhereNode | undefined, condition2: WhereNode | undefined) { if (condition1 && condition2) { return WhereNode.create(AndNode.create(condition1, condition2)); } else if (condition1) { diff --git a/packages/runtime/src/client/functions.ts b/packages/runtime/src/client/functions.ts index 4e2404d7..e05041df 100644 --- a/packages/runtime/src/client/functions.ts +++ b/packages/runtime/src/client/functions.ts @@ -1,19 +1,11 @@ -import { - sql, - ValueNode, - type Expression, - type ExpressionBuilder, -} from 'kysely'; +import { sql, ValueNode, type Expression, type ExpressionBuilder } from 'kysely'; import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import type { ZModelFunction, ZModelFunctionContext } from './options'; // TODO: migrate default value generation functions to here too -export const contains: ZModelFunction = ( - eb: ExpressionBuilder, - args: Expression[] -) => { +export const contains: ZModelFunction = (eb: ExpressionBuilder, args: Expression[]) => { const [field, search, caseInsensitive = false] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -25,17 +17,11 @@ export const contains: ZModelFunction = ( return eb(field, caseInsensitive ? 'ilike' : 'like', searchExpr); }; -export const search: ZModelFunction = ( - _eb: ExpressionBuilder, - _args: Expression[] -) => { +export const search: ZModelFunction = (_eb: ExpressionBuilder, _args: Expression[]) => { throw new Error(`"search" function is not implemented yet`); }; -export const startsWith: ZModelFunction = ( - eb: ExpressionBuilder, - args: Expression[] -) => { +export const startsWith: ZModelFunction = (eb: ExpressionBuilder, args: Expression[]) => { const [field, search] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -46,10 +32,7 @@ export const startsWith: ZModelFunction = ( return eb(field, 'like', eb.fn('CONCAT', [search, sql.lit('%')])); }; -export const endsWith: ZModelFunction = ( - eb: ExpressionBuilder, - args: Expression[] -) => { +export const endsWith: ZModelFunction = (eb: ExpressionBuilder, args: Expression[]) => { const [field, search] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -60,10 +43,7 @@ export const endsWith: ZModelFunction = ( return eb(field, 'like', eb.fn('CONCAT', [sql.lit('%'), search])); }; -export const has: ZModelFunction = ( - eb: ExpressionBuilder, - args: Expression[] -) => { +export const has: ZModelFunction = (eb: ExpressionBuilder, args: Expression[]) => { const [field, search] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -74,10 +54,7 @@ export const has: ZModelFunction = ( return eb(field, '@>', [search]); }; -export const hasEvery: ZModelFunction = ( - eb: ExpressionBuilder, - args: Expression[] -) => { +export const hasEvery: ZModelFunction = (eb: ExpressionBuilder, args: Expression[]) => { const [field, search] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -88,10 +65,7 @@ export const hasEvery: ZModelFunction = ( return eb(field, '@>', search); }; -export const hasSome: ZModelFunction = ( - eb: ExpressionBuilder, - args: Expression[] -) => { +export const hasSome: ZModelFunction = (eb: ExpressionBuilder, args: Expression[]) => { const [field, search] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -105,7 +79,7 @@ export const hasSome: ZModelFunction = ( export const isEmpty: ZModelFunction = ( eb: ExpressionBuilder, args: Expression[], - { dialect }: ZModelFunctionContext + { dialect }: ZModelFunctionContext, ) => { const [field] = args; if (!field) { @@ -117,7 +91,7 @@ export const isEmpty: ZModelFunction = ( export const now: ZModelFunction = ( eb: ExpressionBuilder, _args: Expression[], - { dialect }: ZModelFunctionContext + { dialect }: ZModelFunctionContext, ) => { return match(dialect.provider) .with('postgresql', () => eb.fn('now')) @@ -128,7 +102,7 @@ export const now: ZModelFunction = ( export const currentModel: ZModelFunction = ( _eb: ExpressionBuilder, args: Expression[], - { model }: ZModelFunctionContext + { model }: ZModelFunctionContext, ) => { let result = model; const [casing] = args; @@ -141,7 +115,7 @@ export const currentModel: ZModelFunction = ( export const currentOperation: ZModelFunction = ( _eb: ExpressionBuilder, args: Expression[], - { operation }: ZModelFunctionContext + { operation }: ZModelFunctionContext, ) => { let result: string = operation; const [casing] = args; @@ -153,25 +127,16 @@ export const currentOperation: ZModelFunction = ( function processCasing(casing: Expression, result: string, model: string) { const opNode = casing.toOperationNode(); - invariant( - ValueNode.is(opNode) && typeof opNode.value === 'string', - '"casting" parameter must be a string value' - ); + invariant(ValueNode.is(opNode) && typeof opNode.value === 'string', '"casting" parameter must be a string value'); result = match(opNode.value) .with('original', () => model) .with('upper', () => result.toUpperCase()) .with('lower', () => result.toLowerCase()) - .with( - 'capitalize', - () => `${result.charAt(0).toUpperCase() + result.slice(1)}` - ) - .with( - 'uncapitalize', - () => `${result.charAt(0).toLowerCase() + result.slice(1)}` - ) + .with('capitalize', () => `${result.charAt(0).toUpperCase() + result.slice(1)}`) + .with('uncapitalize', () => `${result.charAt(0).toLowerCase() + result.slice(1)}`) .otherwise(() => { throw new Error( - `Invalid casing value: ${opNode.value}. Must be "original", "upper", "lower", "capitalize", or "uncapitalize".` + `Invalid casing value: ${opNode.value}. Must be "original", "upper", "lower", "capitalize", or "uncapitalize".`, ); }); return result; diff --git a/packages/runtime/src/client/helpers/schema-db-pusher.ts b/packages/runtime/src/client/helpers/schema-db-pusher.ts index 6d607978..4dd3b2d7 100644 --- a/packages/runtime/src/client/helpers/schema-db-pusher.ts +++ b/packages/runtime/src/client/helpers/schema-db-pusher.ts @@ -1,9 +1,4 @@ -import { - CreateTableBuilder, - sql, - type ColumnDataType, - type OnModifyForeignAction, -} from 'kysely'; +import { CreateTableBuilder, sql, type ColumnDataType, type OnModifyForeignAction } from 'kysely'; import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import { @@ -21,58 +16,35 @@ import { requireModel } from '../query-utils'; export class SchemaDbPusher { constructor( private readonly schema: Schema, - private readonly kysely: ToKysely + private readonly kysely: ToKysely, ) {} async push() { await this.kysely.transaction().execute(async (tx) => { - if ( - this.schema.enums && - this.schema.provider.type === 'postgresql' - ) { - for (const [name, enumDef] of Object.entries( - this.schema.enums - )) { - const createEnum = tx.schema - .createType(name) - .asEnum(Object.values(enumDef)); + if (this.schema.enums && this.schema.provider.type === 'postgresql') { + for (const [name, enumDef] of Object.entries(this.schema.enums)) { + const createEnum = tx.schema.createType(name).asEnum(Object.values(enumDef)); // console.log('Creating enum:', createEnum.compile().sql); await createEnum.execute(); } } for (const model of Object.keys(this.schema.models)) { - const createTable = this.createModelTable( - tx, - model as GetModels - ); + const createTable = this.createModelTable(tx, model as GetModels); // console.log('Creating table:', createTable.compile().sql); await createTable.execute(); } }); } - private createModelTable( - kysely: ToKysely, - model: GetModels - ) { + private createModelTable(kysely: ToKysely, model: GetModels) { let table = kysely.schema.createTable(model).ifNotExists(); const modelDef = requireModel(this.schema, model); for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { if (fieldDef.relation) { - table = this.addForeignKeyConstraint( - table, - model, - fieldName, - fieldDef - ); + table = this.addForeignKeyConstraint(table, model, fieldName, fieldDef); } else { - table = this.createModelField( - table, - fieldName, - fieldDef, - modelDef - ); + table = this.createModelField(table, fieldName, fieldDef, modelDef); } } @@ -85,7 +57,7 @@ export class SchemaDbPusher { private addPrimaryKeyConstraint( table: CreateTableBuilder, model: GetModels, - modelDef: ModelDef + modelDef: ModelDef, ) { if (modelDef.idFields.length === 1) { if (Object.values(modelDef.fields).some((f) => f.id)) { @@ -95,19 +67,13 @@ export class SchemaDbPusher { } if (modelDef.idFields.length > 0) { - table = table.addPrimaryKeyConstraint( - `pk_${model}`, - modelDef.idFields - ); + table = table.addPrimaryKeyConstraint(`pk_${model}`, modelDef.idFields); } return table; } - private addUniqueConstraint( - table: CreateTableBuilder, - modelDef: ModelDef - ) { + private addUniqueConstraint(table: CreateTableBuilder, modelDef: ModelDef) { for (const [key, value] of Object.entries(modelDef.uniqueFields)) { invariant(typeof value === 'object', 'expecting an object'); if ('type' in value) { @@ -118,10 +84,7 @@ export class SchemaDbPusher { } } else { // multi-field constraint - table = table.addUniqueConstraint( - `unique_${key}`, - Object.keys(value) - ); + table = table.addUniqueConstraint(`unique_${key}`, Object.keys(value)); } } return table; @@ -131,67 +94,49 @@ export class SchemaDbPusher { table: CreateTableBuilder, fieldName: string, fieldDef: FieldDef, - modelDef: ModelDef + modelDef: ModelDef, ) { - return table.addColumn( - fieldName, - this.mapFieldType(fieldDef), - (col) => { - // @id - if (fieldDef.id && modelDef.idFields.length === 1) { - col = col.primaryKey(); - } + return table.addColumn(fieldName, this.mapFieldType(fieldDef), (col) => { + // @id + if (fieldDef.id && modelDef.idFields.length === 1) { + col = col.primaryKey(); + } - // @default - if (fieldDef.default !== undefined) { - if ( - typeof fieldDef.default === 'object' && - 'kind' in fieldDef.default - ) { - if ( - ExpressionUtils.isCall(fieldDef.default) && - fieldDef.default.function === 'now' - ) { - col = col.defaultTo(sql`CURRENT_TIMESTAMP`); - } - } else { - col = col.defaultTo(fieldDef.default); + // @default + if (fieldDef.default !== undefined) { + if (typeof fieldDef.default === 'object' && 'kind' in fieldDef.default) { + if (ExpressionUtils.isCall(fieldDef.default) && fieldDef.default.function === 'now') { + col = col.defaultTo(sql`CURRENT_TIMESTAMP`); } + } else { + col = col.defaultTo(fieldDef.default); } + } - // @unique - if (fieldDef.unique) { - col = col.unique(); - } - - // nullable - if (!fieldDef.optional && !fieldDef.array) { - col = col.notNull(); - } + // @unique + if (fieldDef.unique) { + col = col.unique(); + } - if ( - this.isAutoIncrement(fieldDef) && - this.schema.provider.type === 'sqlite' - ) { - col = col.autoIncrement(); - } + // nullable + if (!fieldDef.optional && !fieldDef.array) { + col = col.notNull(); + } - return col; + if (this.isAutoIncrement(fieldDef) && this.schema.provider.type === 'sqlite') { + col = col.autoIncrement(); } - ); + + return col; + }); } private mapFieldType(fieldDef: FieldDef) { if (this.schema.enums?.[fieldDef.type]) { - return this.schema.provider.type === 'postgresql' - ? sql.ref(fieldDef.type) - : 'text'; + return this.schema.provider.type === 'postgresql' ? sql.ref(fieldDef.type) : 'text'; } - if ( - this.isAutoIncrement(fieldDef) && - this.schema.provider.type === 'postgresql' - ) { + if (this.isAutoIncrement(fieldDef) && this.schema.provider.type === 'postgresql') { return 'serial'; } @@ -204,9 +149,7 @@ export class SchemaDbPusher { .with('BigInt', () => 'bigint') .with('Decimal', () => 'decimal') .with('DateTime', () => 'timestamp') - .with('Bytes', () => - this.schema.provider.type === 'postgresql' ? 'bytea' : 'blob' - ) + .with('Bytes', () => (this.schema.provider.type === 'postgresql' ? 'bytea' : 'blob')) .otherwise(() => { throw new Error(`Unsupported field type: ${type}`); }); @@ -231,7 +174,7 @@ export class SchemaDbPusher { table: CreateTableBuilder, model: GetModels, fieldName: string, - fieldDef: FieldDef + fieldDef: FieldDef, ) { invariant(fieldDef.relation, 'field must be a relation'); @@ -247,17 +190,13 @@ export class SchemaDbPusher { fieldDef.relation.references, (cb) => { if (fieldDef.relation?.onDelete) { - cb = cb.onDelete( - this.mapCascadeAction(fieldDef.relation.onDelete) - ); + cb = cb.onDelete(this.mapCascadeAction(fieldDef.relation.onDelete)); } if (fieldDef.relation?.onUpdate) { - cb = cb.onUpdate( - this.mapCascadeAction(fieldDef.relation.onUpdate) - ); + cb = cb.onUpdate(this.mapCascadeAction(fieldDef.relation.onUpdate)); } return cb; - } + }, ); return table; } diff --git a/packages/runtime/src/client/options.ts b/packages/runtime/src/client/options.ts index 81f91528..be4ba98d 100644 --- a/packages/runtime/src/client/options.ts +++ b/packages/runtime/src/client/options.ts @@ -1,32 +1,17 @@ -/* eslint-disable @typescript-eslint/ban-types */ - -import type { - Expression, - ExpressionBuilder, - KyselyConfig, - PostgresDialectConfig, - SqliteDialectConfig, -} from 'kysely'; +import type { Expression, ExpressionBuilder, KyselyConfig, PostgresDialectConfig, SqliteDialectConfig } from 'kysely'; import type { Optional } from 'utility-types'; -import type { - DataSourceProvider, - GetModel, - GetModels, - ProcedureDef, - SchemaDef, -} from '../schema'; +import type { DataSourceProvider, GetModel, GetModels, ProcedureDef, SchemaDef } from '../schema'; import type { PrependParameter } from '../utils/type-utils'; import type { ClientContract, CRUD, ProcedureFunc } from './contract'; import type { BaseCrudDialect } from './crud/dialects/base'; import type { RuntimePlugin } from './plugin'; import type { ToKyselySchema } from './query-builder'; -type DialectConfig = - Provider['type'] extends 'sqlite' - ? Optional - : Provider['type'] extends 'postgresql' - ? Optional - : never; +type DialectConfig = Provider['type'] extends 'sqlite' + ? Optional + : Provider['type'] extends 'postgresql' + ? Optional + : never; export type ZModelFunctionContext = { dialect: BaseCrudDialect; @@ -37,7 +22,7 @@ export type ZModelFunctionContext = { export type ZModelFunction = ( eb: ExpressionBuilder, keyof ToKyselySchema>, args: Expression[], - context: ZModelFunctionContext + context: ZModelFunctionContext, ) => Expression; /** @@ -81,12 +66,7 @@ export type ClientOptions = { : {}); export type ComputedFieldsOptions = { - [Model in GetModels as 'computedFields' extends keyof GetModel< - Schema, - Model - > - ? Model - : never]: { + [Model in GetModels as 'computedFields' extends keyof GetModel ? Model : never]: { [Field in keyof Schema['models'][Model]['computedFields']]: PrependParameter< ExpressionBuilder, Model>, Schema['models'][Model]['computedFields'][Field] @@ -95,11 +75,7 @@ export type ComputedFieldsOptions = { }; export type HasComputedFields = - string extends GetModels - ? false - : keyof ComputedFieldsOptions extends never - ? false - : true; + string extends GetModels ? false : keyof ComputedFieldsOptions extends never ? false : true; export type ProceduresOptions = Schema extends { procedures: Record; diff --git a/packages/runtime/src/client/plugin.ts b/packages/runtime/src/client/plugin.ts index e2f6f3a1..962aa260 100644 --- a/packages/runtime/src/client/plugin.ts +++ b/packages/runtime/src/client/plugin.ts @@ -1,10 +1,5 @@ import type { Model } from '@zenstackhq/language/ast'; -import type { - OperationNode, - QueryResult, - RootOperationNode, - UnknownRow, -} from 'kysely'; +import type { OperationNode, QueryResult, RootOperationNode, UnknownRow } from 'kysely'; import type { ClientContract, ToKysely } from '.'; import type { GetModels, SchemaDef } from '../schema'; import type { MaybePromise } from '../utils/type-utils'; @@ -78,29 +73,23 @@ export type OnQueryArgs = QueryContext & { proceed: ProceedQueryFunction; }; -export type PluginBeforeEntityMutationArgs = - MutationHooksArgs & { - entities?: Record[]; - }; +export type PluginBeforeEntityMutationArgs = MutationHooksArgs & { + entities?: Record[]; +}; -export type PluginAfterEntityMutationArgs = - MutationHooksArgs & { - beforeMutationEntities?: Record[]; - afterMutationEntities?: Record[]; - }; +export type PluginAfterEntityMutationArgs = MutationHooksArgs & { + beforeMutationEntities?: Record[]; + afterMutationEntities?: Record[]; +}; export type ProceedQueryFunction = ( queryArgs: unknown, - tx?: ClientContract + tx?: ClientContract, ) => Promise; -export type OnKyselyQueryTransactionCallback = ( - proceed: ProceedKyselyQueryFunction -) => Promise>; +export type OnKyselyQueryTransactionCallback = (proceed: ProceedKyselyQueryFunction) => Promise>; -export type OnKyselyQueryTransaction = ( - callback: OnKyselyQueryTransactionCallback -) => Promise>; +export type OnKyselyQueryTransaction = (callback: OnKyselyQueryTransactionCallback) => Promise>; export type OnKyselyQueryArgs = { kysely: ToKysely; @@ -111,9 +100,7 @@ export type OnKyselyQueryArgs = { transaction: OnKyselyQueryTransaction; }; -export type ProceedKyselyQueryFunction = ( - query: RootOperationNode -) => Promise>; +export type ProceedKyselyQueryFunction = (query: RootOperationNode) => Promise>; /** * ZenStack runtime plugin. @@ -142,26 +129,20 @@ export interface RuntimePlugin { /** * Intercepts a Kysely query. */ - onKyselyQuery?: ( - args: OnKyselyQueryArgs - ) => Promise>; + onKyselyQuery?: (args: OnKyselyQueryArgs) => Promise>; /** * This callback determines whether a mutation should be intercepted, and if so, * what data should be loaded before and after the mutation. */ - mutationInterceptionFilter?: ( - args: MutationHooksArgs - ) => MaybePromise; + mutationInterceptionFilter?: (args: MutationHooksArgs) => MaybePromise; /** * Called before an entity is mutated. * @param args.entity Only available if `loadBeforeMutationEntity` is set to true in the * return value of {@link RuntimePlugin.mutationInterceptionFilter}. */ - beforeEntityMutation?: ( - args: PluginBeforeEntityMutationArgs - ) => MaybePromise; + beforeEntityMutation?: (args: PluginBeforeEntityMutationArgs) => MaybePromise; /** * Called after an entity is mutated. @@ -170,9 +151,7 @@ export interface RuntimePlugin { * @param args.afterMutationEntity Only available if `loadAfterMutationEntity` is set to true in the * return value of {@link RuntimePlugin.mutationInterceptionFilter}. */ - afterEntityMutation?: ( - args: PluginAfterEntityMutationArgs - ) => MaybePromise; + afterEntityMutation?: (args: PluginAfterEntityMutationArgs) => MaybePromise; } // TODO: move to SDK diff --git a/packages/runtime/src/client/promise.ts b/packages/runtime/src/client/promise.ts index 7b210023..00e4f5c2 100644 --- a/packages/runtime/src/client/promise.ts +++ b/packages/runtime/src/client/promise.ts @@ -2,9 +2,7 @@ * Creates a promise that only executes when it's awaited or .then() is called. * @see https://github.com/prisma/prisma/blob/main/packages/client/src/runtime/core/request/createPrismaPromise.ts */ -export function createDeferredPromise( - callback: () => Promise -): Promise { +export function createDeferredPromise(callback: () => Promise): Promise { let promise: Promise | undefined; const cb = () => { try { diff --git a/packages/runtime/src/client/query-builder.ts b/packages/runtime/src/client/query-builder.ts index 74ab8ecd..f049727f 100644 --- a/packages/runtime/src/client/query-builder.ts +++ b/packages/runtime/src/client/query-builder.ts @@ -17,13 +17,8 @@ export type ToKyselySchema = { export type ToKysely = Kysely>; -type ToKyselyTable< - Schema extends SchemaDef, - Model extends GetModels -> = { - [Field in - | ScalarFields - | ForeignKeyFields]: toKyselyFieldType< +type ToKyselyTable> = { + [Field in ScalarFields | ForeignKeyFields]: toKyselyFieldType< Schema, Model, Field @@ -33,32 +28,30 @@ type ToKyselyTable< export type MapBaseType = T extends 'String' ? string : T extends 'Boolean' - ? boolean - : T extends 'Int' | 'Float' - ? number - : T extends 'BigInt' - ? bigint - : T extends 'Decimal' - ? Decimal - : T extends 'DateTime' - ? string - : unknown; + ? boolean + : T extends 'Int' | 'Float' + ? number + : T extends 'BigInt' + ? bigint + : T extends 'Decimal' + ? Decimal + : T extends 'DateTime' + ? string + : unknown; type WrapNull = Null extends true ? T | null : T; type MapType< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields -> = WrapNull< - MapBaseType>, - FieldIsOptional ->; + Field extends GetFields, +> = WrapNull>, FieldIsOptional>; type toKyselyFieldType< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields -> = FieldHasDefault extends true - ? Generated> - : MapType; + Field extends GetFields, +> = + FieldHasDefault extends true + ? Generated> + : MapType; diff --git a/packages/runtime/src/client/query-utils.ts b/packages/runtime/src/client/query-utils.ts index e9a0f58c..f47ac1e7 100644 --- a/packages/runtime/src/client/query-utils.ts +++ b/packages/runtime/src/client/query-utils.ts @@ -15,9 +15,7 @@ export function getModel(schema: SchemaDef, model: string) { } export function requireModel(schema: SchemaDef, model: string) { - const matchedName = Object.keys(schema.models).find( - (k) => k.toLowerCase() === model.toLowerCase() - ); + const matchedName = Object.keys(schema.models).find((k) => k.toLowerCase() === model.toLowerCase()); if (!matchedName) { throw new QueryError(`Model "${model}" not found`); } @@ -37,10 +35,7 @@ export function requireField(schema: SchemaDef, model: string, field: string) { return modelDef.fields[field]; } -export function getIdFields( - schema: SchemaDef, - model: GetModels -) { +export function getIdFields(schema: SchemaDef, model: GetModels) { const modelDef = requireModel(schema, model); return modelDef?.idFields as GetModels[]; } @@ -54,11 +49,7 @@ export function requireIdFields(schema: SchemaDef, model: string) { return result; } -export function getRelationForeignKeyFieldPairs( - schema: SchemaDef, - model: string, - relationField: string -) { +export function getRelationForeignKeyFieldPairs(schema: SchemaDef, model: string, relationField: string) { const fieldDef = requireField(schema, model, relationField); if (!fieldDef?.relation) { @@ -67,9 +58,7 @@ export function getRelationForeignKeyFieldPairs( if (fieldDef.relation.fields) { if (!fieldDef.relation.references) { - throw new InternalError( - `Relation references not defined for field "${relationField}"` - ); + throw new InternalError(`Relation references not defined for field "${relationField}"`); } // this model owns the fk return { @@ -81,31 +70,19 @@ export function getRelationForeignKeyFieldPairs( }; } else { if (!fieldDef.relation.opposite) { - throw new InternalError( - `Opposite relation not defined for field "${relationField}"` - ); + throw new InternalError(`Opposite relation not defined for field "${relationField}"`); } - const oppositeField = requireField( - schema, - fieldDef.type, - fieldDef.relation.opposite - ); + const oppositeField = requireField(schema, fieldDef.type, fieldDef.relation.opposite); if (!oppositeField.relation) { - throw new InternalError( - `Field "${fieldDef.relation.opposite}" is not a relation` - ); + throw new InternalError(`Field "${fieldDef.relation.opposite}" is not a relation`); } if (!oppositeField.relation.fields) { - throw new InternalError( - `Relation fields not defined for field "${relationField}"` - ); + throw new InternalError(`Relation fields not defined for field "${relationField}"`); } if (!oppositeField.relation.references) { - throw new InternalError( - `Relation references not defined for field "${relationField}"` - ); + throw new InternalError(`Relation references not defined for field "${relationField}"`); } // the opposite model owns the fk @@ -119,29 +96,17 @@ export function getRelationForeignKeyFieldPairs( } } -export function isScalarField( - schema: SchemaDef, - model: string, - field: string -): boolean { +export function isScalarField(schema: SchemaDef, model: string, field: string): boolean { const fieldDef = requireField(schema, model, field); return !fieldDef.relation && !fieldDef.foreignKeyFor; } -export function isForeignKeyField( - schema: SchemaDef, - model: string, - field: string -): boolean { +export function isForeignKeyField(schema: SchemaDef, model: string, field: string): boolean { const fieldDef = requireField(schema, model, field); return !!fieldDef.foreignKeyFor; } -export function isRelationField( - schema: SchemaDef, - model: string, - field: string -): boolean { +export function isRelationField(schema: SchemaDef, model: string, field: string): boolean { const fieldDef = requireField(schema, model, field); return !!fieldDef.relation; } @@ -156,9 +121,7 @@ export function getUniqueFields(schema: SchemaDef, model: string) { > = []; for (const [key, value] of Object.entries(modelDef.uniqueFields)) { if (typeof value !== 'object') { - throw new InternalError( - `Invalid unique field definition for "${key}"` - ); + throw new InternalError(`Invalid unique field definition for "${key}"`); } if (typeof value.type === 'string') { @@ -168,31 +131,19 @@ export function getUniqueFields(schema: SchemaDef, model: string) { // compound unique field result.push({ name: key, - defs: Object.fromEntries( - Object.keys(value).map((k) => [ - k, - requireField(schema, model, k), - ]) - ), + defs: Object.fromEntries(Object.keys(value).map((k) => [k, requireField(schema, model, k)])), }); } } return result; } -export function getIdValues( - schema: SchemaDef, - model: string, - data: any -): Record { +export function getIdValues(schema: SchemaDef, model: string, data: any): Record { const idFields = getIdFields(schema, model); if (!idFields) { throw new InternalError(`ID fields not defined for model "${model}"`); } - return idFields.reduce( - (acc, field) => ({ ...acc, [field]: data[field] }), - {} - ); + return idFields.reduce((acc, field) => ({ ...acc, [field]: data[field] }), {}); } export function buildFieldRef( @@ -201,25 +152,19 @@ export function buildFieldRef( field: string, options: ClientOptions, eb: ExpressionBuilder, - modelAlias?: string + modelAlias?: string, ): ExpressionWrapper { const fieldDef = requireField(schema, model, field); if (!fieldDef.computed) { return eb.ref(modelAlias ? `${modelAlias}.${field}` : field); } else { - // eslint-disable-next-line @typescript-eslint/ban-types let computer: Function | undefined; if ('computedFields' in options) { - const computedFields = options.computedFields as Record< - string, - any - >; + const computedFields = options.computedFields as Record; computer = computedFields?.[model]?.[field]; } if (!computer) { - throw new QueryError( - `Computed field "${field}" implementation not provided` - ); + throw new QueryError(`Computed field "${field}" implementation not provided`); } return computer(eb); } @@ -242,13 +187,9 @@ export function buildJoinPairs( model: string, modelAlias: string, relationField: string, - relationModelAlias: string + relationModelAlias: string, ): [string, string][] { - const { keyPairs, ownedByModel } = getRelationForeignKeyFieldPairs( - schema, - model, - relationField - ); + const { keyPairs, ownedByModel } = getRelationForeignKeyFieldPairs(schema, model, relationField); return keyPairs.map(({ fk, pk }) => { if (ownedByModel) { @@ -261,31 +202,17 @@ export function buildJoinPairs( }); } -export function makeDefaultOrderBy( - schema: SchemaDef, - model: string -) { +export function makeDefaultOrderBy(schema: SchemaDef, model: string) { const idFields = getIdFields(schema, model); - return idFields.map( - (f) => - ({ [f]: 'asc' } as OrderBy, true, false>) - ); + return idFields.map((f) => ({ [f]: 'asc' }) as OrderBy, true, false>); } -export function getManyToManyRelation( - schema: SchemaDef, - model: string, - field: string -) { +export function getManyToManyRelation(schema: SchemaDef, model: string, field: string) { const fieldDef = requireField(schema, model, field); if (!fieldDef.array || !fieldDef.relation?.opposite) { return undefined; } - const oppositeFieldDef = requireField( - schema, - fieldDef.type, - fieldDef.relation.opposite - ); + const oppositeFieldDef = requireField(schema, fieldDef.type, fieldDef.relation.opposite); if (oppositeFieldDef.array) { // Prisma's convention for many-to-many relation: // - model are sorted alphabetically by name @@ -309,11 +236,7 @@ export function getManyToManyRelation( /** * Convert filter like `{ id1_id2: { id1: 1, id2: 1 } }` to `{ id1: 1, id2: 1 }` */ -export function flattenCompoundUniqueFilters( - schema: SchemaDef, - model: string, - filter: unknown -) { +export function flattenCompoundUniqueFilters(schema: SchemaDef, model: string, filter: unknown) { if (typeof filter !== 'object' || !filter) { return filter; } diff --git a/packages/runtime/src/client/result-processor.ts b/packages/runtime/src/client/result-processor.ts index 92c2b3d8..6899346d 100644 --- a/packages/runtime/src/client/result-processor.ts +++ b/packages/runtime/src/client/result-processor.ts @@ -34,8 +34,7 @@ export class ResultProcessor { if (key === '_count') { // underlying database provider may return string for count - data[key] = - typeof value === 'string' ? JSON.parse(value) : value; + data[key] = typeof value === 'string' ? JSON.parse(value) : value; continue; } @@ -81,10 +80,7 @@ export class ResultProcessor { return value; } } - return this.doProcessResult( - relationData, - fieldDef.type as GetModels - ); + return this.doProcessResult(relationData, fieldDef.type as GetModels); } private transformScalar(value: unknown, type: BuiltinType) { @@ -102,10 +98,8 @@ export class ResultProcessor { return value; } invariant( - typeof value === 'string' || - typeof value === 'number' || - value instanceof Decimal, - `Expected string, number or Decimal, got ${typeof value}` + typeof value === 'string' || typeof value === 'number' || value instanceof Decimal, + `Expected string, number or Decimal, got ${typeof value}`, ); return new Decimal(value); } @@ -116,7 +110,7 @@ export class ResultProcessor { } invariant( typeof value === 'string' || typeof value === 'number', - `Expected string or number, got ${typeof value}` + `Expected string or number, got ${typeof value}`, ); return BigInt(value); } @@ -140,13 +134,7 @@ export class ResultProcessor { } private fixReversedResult(data: any, model: GetModels, args: any) { - if ( - Array.isArray(data) && - typeof args === 'object' && - args && - args.take !== undefined && - args.take < 0 - ) { + if (Array.isArray(data) && typeof args === 'object' && args && args.take !== undefined && args.take < 0) { data.reverse(); } @@ -164,11 +152,7 @@ export class ResultProcessor { if (!fieldDef?.relation) { continue; } - this.fixReversedResult( - row[field], - fieldDef.type as GetModels, - value - ); + this.fixReversedResult(row[field], fieldDef.type as GetModels, value); } } } diff --git a/packages/runtime/src/plugins/policy/errors.ts b/packages/runtime/src/plugins/policy/errors.ts index 0c0c85f9..df1feab6 100644 --- a/packages/runtime/src/plugins/policy/errors.ts +++ b/packages/runtime/src/plugins/policy/errors.ts @@ -4,10 +4,8 @@ export class RejectedByPolicyError extends Error { constructor( public readonly model: string | undefined, - public readonly reason?: string + public readonly reason?: string, ) { - super( - reason ?? `Operation rejected by policy${model ? ': ' + model : ''}` - ); + super(reason ?? `Operation rejected by policy${model ? ': ' + model : ''}`); } } diff --git a/packages/runtime/src/plugins/policy/expression-evaluator.ts b/packages/runtime/src/plugins/policy/expression-evaluator.ts index a2d64e16..77b1ec1e 100644 --- a/packages/runtime/src/plugins/policy/expression-evaluator.ts +++ b/packages/runtime/src/plugins/policy/expression-evaluator.ts @@ -23,27 +23,13 @@ type ExpressionEvaluatorContext = { export class ExpressionEvaluator { evaluate(expression: Expression, context: ExpressionEvaluatorContext): any { const result = match(expression) - .when(ExpressionUtils.isArray, (expr) => - this.evaluateArray(expr, context) - ) - .when(ExpressionUtils.isBinary, (expr) => - this.evaluateBinary(expr, context) - ) - .when(ExpressionUtils.isField, (expr) => - this.evaluateField(expr, context) - ) - .when(ExpressionUtils.isLiteral, (expr) => - this.evaluateLiteral(expr) - ) - .when(ExpressionUtils.isMember, (expr) => - this.evaluateMember(expr, context) - ) - .when(ExpressionUtils.isUnary, (expr) => - this.evaluateUnary(expr, context) - ) - .when(ExpressionUtils.isCall, (expr) => - this.evaluateCall(expr, context) - ) + .when(ExpressionUtils.isArray, (expr) => this.evaluateArray(expr, context)) + .when(ExpressionUtils.isBinary, (expr) => this.evaluateBinary(expr, context)) + .when(ExpressionUtils.isField, (expr) => this.evaluateField(expr, context)) + .when(ExpressionUtils.isLiteral, (expr) => this.evaluateLiteral(expr)) + .when(ExpressionUtils.isMember, (expr) => this.evaluateMember(expr, context)) + .when(ExpressionUtils.isUnary, (expr) => this.evaluateUnary(expr, context)) + .when(ExpressionUtils.isCall, (expr) => this.evaluateCall(expr, context)) .when(ExpressionUtils.isThis, () => context.thisValue) .when(ExpressionUtils.isNull, () => null) .exhaustive(); @@ -51,32 +37,21 @@ export class ExpressionEvaluator { return result ?? null; } - private evaluateCall( - expr: CallExpression, - context: ExpressionEvaluatorContext - ): any { + private evaluateCall(expr: CallExpression, context: ExpressionEvaluatorContext): any { if (expr.function === 'auth') { return context.auth; } else { - throw new Error( - `Unsupported call expression function: ${expr.function}` - ); + throw new Error(`Unsupported call expression function: ${expr.function}`); } } - private evaluateUnary( - expr: UnaryExpression, - context: ExpressionEvaluatorContext - ) { + private evaluateUnary(expr: UnaryExpression, context: ExpressionEvaluatorContext) { return match(expr.op) .with('!', () => !this.evaluate(expr.operand, context)) .exhaustive(); } - private evaluateMember( - expr: MemberExpression, - context: ExpressionEvaluatorContext - ) { + private evaluateMember(expr: MemberExpression, context: ExpressionEvaluatorContext) { let val = this.evaluate(expr.receiver, context); for (const member of expr.members) { val = val?.[member]; @@ -88,24 +63,15 @@ export class ExpressionEvaluator { return expr.value; } - private evaluateField( - expr: FieldExpression, - context: ExpressionEvaluatorContext - ): any { + private evaluateField(expr: FieldExpression, context: ExpressionEvaluatorContext): any { return context.thisValue?.[expr.field]; } - private evaluateArray( - expr: ArrayExpression, - context: ExpressionEvaluatorContext - ) { + private evaluateArray(expr: ArrayExpression, context: ExpressionEvaluatorContext) { return expr.items.map((item) => this.evaluate(item, context)); } - private evaluateBinary( - expr: BinaryExpression, - context: ExpressionEvaluatorContext - ) { + private evaluateBinary(expr: BinaryExpression, context: ExpressionEvaluatorContext) { if (expr.op === '?' || expr.op === '!' || expr.op === '^') { return this.evaluateCollectionPredicate(expr, context); } @@ -124,24 +90,15 @@ export class ExpressionEvaluator { .with('||', () => left || right) .with('in', () => { const _right = right ?? []; - invariant( - Array.isArray(_right), - 'expected array for "in" operator' - ); + invariant(Array.isArray(_right), 'expected array for "in" operator'); return _right.includes(left); }) .exhaustive(); } - private evaluateCollectionPredicate( - expr: BinaryExpression, - context: ExpressionEvaluatorContext - ) { + private evaluateCollectionPredicate(expr: BinaryExpression, context: ExpressionEvaluatorContext) { const op = expr.op; - invariant( - op === '?' || op === '!' || op === '^', - 'expected "?" or "!" or "^" operator' - ); + invariant(op === '?' || op === '!' || op === '^', 'expected "?" or "!" or "^" operator'); const left = this.evaluate(expr.left, context); if (!left) { @@ -151,16 +108,8 @@ export class ExpressionEvaluator { invariant(Array.isArray(left), 'expected array'); return match(op) - .with('?', () => - left.some((item: any) => - this.evaluate(expr.right, { ...context, thisValue: item }) - ) - ) - .with('!', () => - left.every((item: any) => - this.evaluate(expr.right, { ...context, thisValue: item }) - ) - ) + .with('?', () => left.some((item: any) => this.evaluate(expr.right, { ...context, thisValue: item }))) + .with('!', () => left.every((item: any) => this.evaluate(expr.right, { ...context, thisValue: item }))) .with( '^', () => @@ -168,8 +117,8 @@ export class ExpressionEvaluator { this.evaluate(expr.right, { ...context, thisValue: item, - }) - ) + }), + ), ) .exhaustive(); } diff --git a/packages/runtime/src/plugins/policy/expression-transformer.ts b/packages/runtime/src/plugins/policy/expression-transformer.ts index 78a694e2..05f40bf1 100644 --- a/packages/runtime/src/plugins/policy/expression-transformer.ts +++ b/packages/runtime/src/plugins/policy/expression-transformer.ts @@ -25,10 +25,7 @@ import { getCrudDialect } from '../../client/crud/dialects'; import type { BaseCrudDialect } from '../../client/crud/dialects/base'; import { InternalError, QueryError } from '../../client/errors'; import type { ClientOptions } from '../../client/options'; -import { - getRelationForeignKeyFieldPairs, - requireField, -} from '../../client/query-utils'; +import { getRelationForeignKeyFieldPairs, requireField } from '../../client/query-utils'; import { ExpressionUtils, type ArrayExpression, @@ -66,11 +63,7 @@ const expressionHandlers = new Map(); // expression handler decorator function expr(kind: Expression['kind']) { - return function ( - _target: unknown, - _propertyKey: string, - descriptor: PropertyDescriptor - ) { + return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) { if (!expressionHandlers.get(kind)) { expressionHandlers.set(kind, descriptor); } @@ -84,24 +77,19 @@ export class ExpressionTransformer { constructor( private readonly schema: Schema, private readonly clientOptions: ClientOptions, - private readonly auth: unknown | undefined + private readonly auth: unknown | undefined, ) { this.dialect = getCrudDialect(this.schema, this.clientOptions); } get authType() { if (!this.schema.authType) { - throw new InternalError( - 'Schema does not have an "authType" specified' - ); + throw new InternalError('Schema does not have an "authType" specified'); } return this.schema.authType; } - transform( - expression: Expression, - context: ExpressionTransformerContext - ): OperationNode { + transform(expression: Expression, context: ExpressionTransformerContext): OperationNode { const handler = expressionHandlers.get(expression.kind); if (!handler) { throw new Error(`Unsupported expression kind: ${expression.kind}`); @@ -114,31 +102,19 @@ export class ExpressionTransformer { private _literal(expr: LiteralExpression) { return this.transformValue( expr.value, - typeof expr.value === 'string' - ? 'String' - : typeof expr.value === 'boolean' - ? 'Boolean' - : 'Int' + typeof expr.value === 'string' ? 'String' : typeof expr.value === 'boolean' ? 'Boolean' : 'Int', ); } @expr('array') // @ts-expect-error - private _array( - expr: ArrayExpression, - context: ExpressionTransformerContext - ) { - return ValueListNode.create( - expr.items.map((item) => this.transform(item, context)) - ); + private _array(expr: ArrayExpression, context: ExpressionTransformerContext) { + return ValueListNode.create(expr.items.map((item) => this.transform(item, context))); } @expr('field') // @ts-expect-error - private _field( - expr: FieldExpression, - context: ExpressionTransformerContext - ) { + private _field(expr: FieldExpression, context: ExpressionTransformerContext) { const fieldDef = requireField(this.schema, context.model, expr.field); if (!fieldDef.relation) { if (context.thisEntity) { @@ -148,11 +124,7 @@ export class ExpressionTransformer { } } else { const { memberFilter, memberSelect, ...restContext } = context; - const relation = this.transformRelationAccess( - expr.field, - fieldDef.type, - restContext - ); + const relation = this.transformRelationAccess(expr.field, fieldDef.type, restContext); return { ...relation, where: this.mergeWhere(relation.where, memberFilter), @@ -161,19 +133,14 @@ export class ExpressionTransformer { } } - private mergeWhere( - where: WhereNode | undefined, - memberFilter: OperationNode | undefined - ) { + private mergeWhere(where: WhereNode | undefined, memberFilter: OperationNode | undefined) { if (!where) { return WhereNode.create(memberFilter ?? trueNode(this.dialect)); } if (!memberFilter) { return where; } - return WhereNode.create( - conjunction(this.dialect, [where.where, memberFilter]) - ); + return WhereNode.create(conjunction(this.dialect, [where.where, memberFilter])); } @expr('null') @@ -184,20 +151,11 @@ export class ExpressionTransformer { @expr('binary') // @ts-ignore - private _binary( - expr: BinaryExpression, - context: ExpressionTransformerContext - ) { + private _binary(expr: BinaryExpression, context: ExpressionTransformerContext) { if (expr.op === '&&') { - return conjunction(this.dialect, [ - this.transform(expr.left, context), - this.transform(expr.right, context), - ]); + return conjunction(this.dialect, [this.transform(expr.left, context), this.transform(expr.right, context)]); } else if (expr.op === '||') { - return disjunction(this.dialect, [ - this.transform(expr.left, context), - this.transform(expr.right, context), - ]); + return disjunction(this.dialect, [this.transform(expr.left, context), this.transform(expr.right, context)]); } if (this.isAuthCall(expr.left) || this.isAuthCall(expr.right)) { @@ -218,17 +176,13 @@ export class ExpressionTransformer { return this.transformValue(false, 'Boolean'); } else { if (ValueListNode.is(right)) { - return BinaryOperationNode.create( - left, - OperatorNode.create('in'), - right - ); + return BinaryOperationNode.create(left, OperatorNode.create('in'), right); } else { // array contains return BinaryOperationNode.create( left, OperatorNode.create('='), - FunctionNode.create('any', [right]) + FunctionNode.create('any', [right]), ); } } @@ -236,45 +190,19 @@ export class ExpressionTransformer { if (this.isNullNode(right)) { return expr.op === '==' - ? BinaryOperationNode.create( - left, - OperatorNode.create('is'), - right - ) - : BinaryOperationNode.create( - left, - OperatorNode.create('is not'), - right - ); + ? BinaryOperationNode.create(left, OperatorNode.create('is'), right) + : BinaryOperationNode.create(left, OperatorNode.create('is not'), right); } else if (this.isNullNode(left)) { return expr.op === '==' - ? BinaryOperationNode.create( - right, - OperatorNode.create('is'), - ValueNode.createImmediate(null) - ) - : BinaryOperationNode.create( - right, - OperatorNode.create('is not'), - ValueNode.createImmediate(null) - ); + ? BinaryOperationNode.create(right, OperatorNode.create('is'), ValueNode.createImmediate(null)) + : BinaryOperationNode.create(right, OperatorNode.create('is not'), ValueNode.createImmediate(null)); } - return BinaryOperationNode.create( - left, - this.transformOperator(op), - right - ); + return BinaryOperationNode.create(left, this.transformOperator(op), right); } - private transformCollectionPredicate( - expr: BinaryExpression, - context: ExpressionTransformerContext - ) { - invariant( - expr.op === '?' || expr.op === '!' || expr.op === '^', - 'expected "?" or "!" or "^" operator' - ); + private transformCollectionPredicate(expr: BinaryExpression, context: ExpressionTransformerContext) { + invariant(expr.op === '?' || expr.op === '!' || expr.op === '^', 'expected "?" or "!" or "^" operator'); if (this.isAuthCall(expr.left) || this.isAuthMember(expr.left)) { const value = new ExpressionEvaluator().evaluate(expr, { @@ -284,33 +212,20 @@ export class ExpressionTransformer { } invariant( - ExpressionUtils.isField(expr.left) || - ExpressionUtils.isMember(expr.left), - 'left operand must be field or member access' + ExpressionUtils.isField(expr.left) || ExpressionUtils.isMember(expr.left), + 'left operand must be field or member access', ); let newContextModel: string; if (ExpressionUtils.isField(expr.left)) { - const fieldDef = requireField( - this.schema, - context.model, - expr.left.field - ); + const fieldDef = requireField(this.schema, context.model, expr.left.field); newContextModel = fieldDef.type; } else { invariant(ExpressionUtils.isField(expr.left.receiver)); - const fieldDef = requireField( - this.schema, - context.model, - expr.left.receiver.field - ); + const fieldDef = requireField(this.schema, context.model, expr.left.receiver.field); newContextModel = fieldDef.type; for (const member of expr.left.members) { - const memberDef = requireField( - this.schema, - newContextModel, - member - ); + const memberDef = requireField(this.schema, newContextModel, member); newContextModel = memberDef.type; } } @@ -326,39 +241,17 @@ export class ExpressionTransformer { predicateFilter = logicalNot(predicateFilter); } - const count = FunctionNode.create('count', [ - ValueNode.createImmediate(1), - ]); + const count = FunctionNode.create('count', [ValueNode.createImmediate(1)]); const predicateResult = match(expr.op) - .with('?', () => - BinaryOperationNode.create( - count, - OperatorNode.create('>'), - ValueNode.createImmediate(0) - ) - ) - .with('!', () => - BinaryOperationNode.create( - count, - OperatorNode.create('='), - ValueNode.createImmediate(0) - ) - ) - .with('^', () => - BinaryOperationNode.create( - count, - OperatorNode.create('='), - ValueNode.createImmediate(0) - ) - ) + .with('?', () => BinaryOperationNode.create(count, OperatorNode.create('>'), ValueNode.createImmediate(0))) + .with('!', () => BinaryOperationNode.create(count, OperatorNode.create('='), ValueNode.createImmediate(0))) + .with('^', () => BinaryOperationNode.create(count, OperatorNode.create('='), ValueNode.createImmediate(0))) .exhaustive(); return this.transform(expr.left, { ...context, - memberSelect: SelectionNode.create( - AliasNode.create(predicateResult, IdentifierNode.create('$t')) - ), + memberSelect: SelectionNode.create(AliasNode.create(predicateResult, IdentifierNode.create('$t'))), memberFilter: predicateFilter, }); } @@ -375,33 +268,25 @@ export class ExpressionTransformer { } if (ExpressionUtils.isNull(other)) { - return this.transformValue( - expr.op === '==' ? !this.auth : !!this.auth, - 'Boolean' - ); + return this.transformValue(expr.op === '==' ? !this.auth : !!this.auth, 'Boolean'); } else { throw new Error('Unsupported binary expression with `auth()`'); } } private transformValue(value: unknown, type: BuiltinType) { - return ValueNode.create( - this.dialect.transformPrimitive(value, type) ?? null - ); + return ValueNode.create(this.dialect.transformPrimitive(value, type) ?? null); } @expr('unary') // @ts-ignore - private _unary( - expr: UnaryExpression, - context: ExpressionTransformerContext - ) { + private _unary(expr: UnaryExpression, context: ExpressionTransformerContext) { // only '!' operator for now invariant(expr.op === '!', 'only "!" operator is supported'); return BinaryOperationNode.create( this.transform(expr.operand, context), this.transformOperator('!='), - trueNode(this.dialect) + trueNode(this.dialect), ); } @@ -414,18 +299,12 @@ export class ExpressionTransformer { @expr('call') // @ts-ignore - private _call( - expr: CallExpression, - context: ExpressionTransformerContext - ) { + private _call(expr: CallExpression, context: ExpressionTransformerContext) { const result = this.transformCall(expr, context); return result.toOperationNode(); } - private transformCall( - expr: CallExpression, - context: ExpressionTransformerContext - ) { + private transformCall(expr: CallExpression, context: ExpressionTransformerContext) { const func = this.clientOptions.functions?.[expr.function]; if (!func) { throw new QueryError(`Function not implemented: ${expr.function}`); @@ -433,30 +312,26 @@ export class ExpressionTransformer { const eb = expressionBuilder(); return func( eb, - (expr.args ?? []).map((arg) => - this.transformCallArg(eb, arg, context) - ), + (expr.args ?? []).map((arg) => this.transformCallArg(eb, arg, context)), { dialect: this.dialect, model: context.model, operation: context.operation, - } + }, ); } private transformCallArg( eb: ExpressionBuilder, arg: Expression, - context: ExpressionTransformerContext + context: ExpressionTransformerContext, ): OperandExpression { if (ExpressionUtils.isLiteral(arg)) { return eb.val(arg.value); } if (ExpressionUtils.isField(arg)) { - return context.thisEntityRaw - ? eb.val(context.thisEntityRaw[arg.field]) - : eb.ref(arg.field); + return context.thisEntityRaw ? eb.val(context.thisEntityRaw[arg.field]) : eb.ref(arg.field); } if (ExpressionUtils.isCall(arg)) { @@ -464,11 +339,7 @@ export class ExpressionTransformer { } if (this.isAuthMember(arg)) { - const valNode = this.valueMemberAccess( - context.auth, - arg as MemberExpression, - this.authType - ); + const valNode = this.valueMemberAccess(context.auth, arg as MemberExpression, this.authType); return valNode ? eb.val(valNode.value) : eb.val(null); } @@ -481,34 +352,21 @@ export class ExpressionTransformer { @expr('member') // @ts-ignore - private _member( - expr: MemberExpression, - context: ExpressionTransformerContext - ) { + private _member(expr: MemberExpression, context: ExpressionTransformerContext) { // auth() member access if (this.isAuthCall(expr.receiver)) { return this.valueMemberAccess(this.auth, expr, this.authType); } - invariant( - ExpressionUtils.isField(expr.receiver), - 'expect receiver to be field expression' - ); + invariant(ExpressionUtils.isField(expr.receiver), 'expect receiver to be field expression'); const { memberFilter, memberSelect, ...restContext } = context; const receiver = this.transform(expr.receiver, restContext); - invariant( - SelectQueryNode.is(receiver), - 'expected receiver to be select query' - ); + invariant(SelectQueryNode.is(receiver), 'expected receiver to be select query'); // relation member access - const receiverField = requireField( - this.schema, - context.model, - expr.receiver.field - ); + const receiverField = requireField(this.schema, context.model, expr.receiver.field); // traverse forward to collect member types const memberFields: { fromModel: string; fieldDef: FieldDef }[] = []; @@ -519,38 +377,27 @@ export class ExpressionTransformer { currType = fieldDef.type; } - let currNode: SelectQueryNode | ColumnNode | ReferenceNode | undefined = - undefined; + let currNode: SelectQueryNode | ColumnNode | ReferenceNode | undefined = undefined; for (let i = expr.members.length - 1; i >= 0; i--) { const member = expr.members[i]!; const { fieldDef, fromModel } = memberFields[i]!; if (fieldDef.relation) { - const relation = this.transformRelationAccess( - member, - fieldDef.type, - { - ...restContext, - model: fromModel as GetModels, - alias: undefined, - thisEntity: undefined, - } - ); + const relation = this.transformRelationAccess(member, fieldDef.type, { + ...restContext, + model: fromModel as GetModels, + alias: undefined, + thisEntity: undefined, + }); if (currNode) { - invariant( - SelectQueryNode.is(currNode), - 'expected select query node' - ); + invariant(SelectQueryNode.is(currNode), 'expected select query node'); currNode = { ...relation, selections: [ SelectionNode.create( - AliasNode.create( - currNode, - IdentifierNode.create(expr.members[i + 1]!) - ) + AliasNode.create(currNode, IdentifierNode.create(expr.members[i + 1]!)), ), ], }; @@ -559,20 +406,12 @@ export class ExpressionTransformer { currNode = { ...relation, where: this.mergeWhere(relation.where, memberFilter), - selections: memberSelect - ? [memberSelect] - : relation.selections, + selections: memberSelect ? [memberSelect] : relation.selections, }; } } else { - invariant( - i === expr.members.length - 1, - 'plain field access must be the last segment' - ); - invariant( - !currNode, - 'plain field access must be the last segment' - ); + invariant(i === expr.members.length - 1, 'plain field access must be the last segment'); + invariant(!currNode, 'plain field access must be the last segment'); currNode = ColumnNode.create(member); } @@ -580,19 +419,11 @@ export class ExpressionTransformer { return { ...receiver, - selections: [ - SelectionNode.create( - AliasNode.create(currNode!, IdentifierNode.create('$t')) - ), - ], + selections: [SelectionNode.create(AliasNode.create(currNode!, IdentifierNode.create('$t')))], }; } - private valueMemberAccess( - receiver: any, - expr: MemberExpression, - receiverType: string - ) { + private valueMemberAccess(receiver: any, expr: MemberExpression, receiverType: string) { if (!receiver) { return ValueNode.createImmediate(null); } @@ -610,14 +441,10 @@ export class ExpressionTransformer { private transformRelationAccess( field: string, relationModel: string, - context: ExpressionTransformerContext + context: ExpressionTransformerContext, ): SelectQueryNode { const fromModel = context.model; - const { keyPairs, ownedByModel } = getRelationForeignKeyFieldPairs( - this.schema, - fromModel, - field - ); + const { keyPairs, ownedByModel } = getRelationForeignKeyFieldPairs(this.schema, fromModel, field); if (context.thisEntity) { let condition: OperationNode; @@ -626,28 +453,22 @@ export class ExpressionTransformer { this.dialect, keyPairs.map(({ fk, pk }) => BinaryOperationNode.create( - ReferenceNode.create( - ColumnNode.create(pk), - TableNode.create(relationModel) - ), + ReferenceNode.create(ColumnNode.create(pk), TableNode.create(relationModel)), OperatorNode.create('='), - context.thisEntity![fk]! - ) - ) + context.thisEntity![fk]!, + ), + ), ); } else { condition = conjunction( this.dialect, keyPairs.map(({ fk, pk }) => BinaryOperationNode.create( - ReferenceNode.create( - ColumnNode.create(fk), - TableNode.create(relationModel) - ), + ReferenceNode.create(ColumnNode.create(fk), TableNode.create(relationModel)), OperatorNode.create('='), - context.thisEntity![pk]! - ) - ) + context.thisEntity![pk]!, + ), + ), ); } @@ -664,17 +485,11 @@ export class ExpressionTransformer { this.dialect, keyPairs.map(({ fk, pk }) => BinaryOperationNode.create( - ReferenceNode.create( - ColumnNode.create(fk), - TableNode.create(context.alias ?? fromModel) - ), + ReferenceNode.create(ColumnNode.create(fk), TableNode.create(context.alias ?? fromModel)), OperatorNode.create('='), - ReferenceNode.create( - ColumnNode.create(pk), - TableNode.create(relationModel) - ) - ) - ) + ReferenceNode.create(ColumnNode.create(pk), TableNode.create(relationModel)), + ), + ), ); } else { // `relationModel` owns the fk @@ -682,17 +497,11 @@ export class ExpressionTransformer { this.dialect, keyPairs.map(({ fk, pk }) => BinaryOperationNode.create( - ReferenceNode.create( - ColumnNode.create(pk), - TableNode.create(context.alias ?? fromModel) - ), + ReferenceNode.create(ColumnNode.create(pk), TableNode.create(context.alias ?? fromModel)), OperatorNode.create('='), - ReferenceNode.create( - ColumnNode.create(fk), - TableNode.create(relationModel) - ) - ) - ) + ReferenceNode.create(ColumnNode.create(fk), TableNode.create(relationModel)), + ), + ), ); } @@ -704,14 +513,8 @@ export class ExpressionTransformer { } } - private createColumnRef( - column: string, - context: ExpressionTransformerContext - ): ReferenceNode { - return ReferenceNode.create( - ColumnNode.create(column), - TableNode.create(context.alias ?? context.model) - ); + private createColumnRef(column: string, context: ExpressionTransformerContext): ReferenceNode { + return ReferenceNode.create(ColumnNode.create(column), TableNode.create(context.alias ?? context.model)); } private isAuthCall(value: unknown): value is CallExpression { diff --git a/packages/runtime/src/plugins/policy/plugin.ts b/packages/runtime/src/plugins/policy/plugin.ts index ed731d87..15b35454 100644 --- a/packages/runtime/src/plugins/policy/plugin.ts +++ b/packages/runtime/src/plugins/policy/plugin.ts @@ -1,13 +1,8 @@ -import { - type OnKyselyQueryArgs, - type RuntimePlugin, -} from '../../client/plugin'; +import { type OnKyselyQueryArgs, type RuntimePlugin } from '../../client/plugin'; import type { SchemaDef } from '../../schema'; import { PolicyHandler } from './policy-handler'; -export class PolicyPlugin - implements RuntimePlugin -{ +export class PolicyPlugin implements RuntimePlugin { get id() { return 'policy'; } @@ -20,12 +15,7 @@ export class PolicyPlugin return 'Enforces access policies defined in the schema.'; } - onKyselyQuery({ - query, - client, - proceed, - transaction, - }: OnKyselyQueryArgs) { + onKyselyQuery({ query, client, proceed, transaction }: OnKyselyQueryArgs) { const handler = new PolicyHandler(client); return handler.handle(query, proceed, transaction); } diff --git a/packages/runtime/src/plugins/policy/policy-handler.ts b/packages/runtime/src/plugins/policy/policy-handler.ts index 03812d2f..f3cae07e 100644 --- a/packages/runtime/src/plugins/policy/policy-handler.ts +++ b/packages/runtime/src/plugins/policy/policy-handler.ts @@ -29,73 +29,35 @@ import type { CRUD } from '../../client/contract'; import { getCrudDialect } from '../../client/crud/dialects'; import type { BaseCrudDialect } from '../../client/crud/dialects/base'; import { InternalError } from '../../client/errors'; -import type { - OnKyselyQueryTransaction, - ProceedKyselyQueryFunction, -} from '../../client/plugin'; -import { - getIdFields, - requireField, - requireModel, -} from '../../client/query-utils'; -import { - ExpressionUtils, - type BuiltinType, - type Expression, - type GetModels, - type SchemaDef, -} from '../../schema'; +import type { OnKyselyQueryTransaction, ProceedKyselyQueryFunction } from '../../client/plugin'; +import { getIdFields, requireField, requireModel } from '../../client/query-utils'; +import { ExpressionUtils, type BuiltinType, type Expression, type GetModels, type SchemaDef } from '../../schema'; import { ColumnCollector } from './column-collector'; import { RejectedByPolicyError } from './errors'; import { ExpressionTransformer } from './expression-transformer'; import type { Policy, PolicyOperation } from './types'; -import { - buildIsFalse, - conjunction, - disjunction, - falseNode, - getTableName, -} from './utils'; - -export type CrudQueryNode = - | SelectQueryNode - | InsertQueryNode - | UpdateQueryNode - | DeleteQueryNode; - -export type MutationQueryNode = - | InsertQueryNode - | UpdateQueryNode - | DeleteQueryNode; - -export class PolicyHandler< - Schema extends SchemaDef -> extends OperationNodeTransformer { +import { buildIsFalse, conjunction, disjunction, falseNode, getTableName } from './utils'; + +export type CrudQueryNode = SelectQueryNode | InsertQueryNode | UpdateQueryNode | DeleteQueryNode; + +export type MutationQueryNode = InsertQueryNode | UpdateQueryNode | DeleteQueryNode; + +export class PolicyHandler extends OperationNodeTransformer { private readonly dialect: BaseCrudDialect; constructor(private readonly client: ClientContract) { super(); - this.dialect = getCrudDialect( - this.client.$schema, - this.client.$options - ); + this.dialect = getCrudDialect(this.client.$schema, this.client.$options); } get kysely() { return this.client.$qb; } - async handle( - node: RootOperationNode, - proceed: ProceedKyselyQueryFunction, - transaction: OnKyselyQueryTransaction - ) { + async handle(node: RootOperationNode, proceed: ProceedKyselyQueryFunction, transaction: OnKyselyQueryTransaction) { if (!this.isCrudQueryNode(node)) { // non CRUD queries are not allowed - throw new RejectedByPolicyError( - undefined, - 'non-CRUD queries are not allowed' - ); + throw new RejectedByPolicyError(undefined, 'non-CRUD queries are not allowed'); } if (!this.isMutationQueryNode(node)) { @@ -108,10 +70,7 @@ export class PolicyHandler< if (InsertQueryNode.is(node)) { // reject create if unconditional deny - const constCondition = this.tryGetConstantPolicy( - mutationModel, - 'create' - ); + const constCondition = this.tryGetConstantPolicy(mutationModel, 'create'); if (constCondition === false) { throw new RejectedByPolicyError(mutationModel); } else if (constCondition === undefined) { @@ -135,11 +94,7 @@ export class PolicyHandler< const result = await txProceed(transformedNode); if (!this.onlyReturningId(node)) { - const readBackResult = await this.processReadBack( - node, - result, - txProceed - ); + const readBackResult = await this.processReadBack(node, result, txProceed); if (readBackResult.rows.length !== result.rows.length) { readBackError = true; } @@ -150,10 +105,7 @@ export class PolicyHandler< }); if (readBackError) { - throw new RejectedByPolicyError( - mutationModel, - 'result is not allowed to be read back' - ); + throw new RejectedByPolicyError(mutationModel, 'result is not allowed to be read back'); } return result; @@ -163,37 +115,27 @@ export class PolicyHandler< if (!node.returning) { return true; } - const idFields = getIdFields( - this.client.$schema, - this.getMutationModel(node) - ); + const idFields = getIdFields(this.client.$schema, this.getMutationModel(node)); const collector = new ColumnCollector(); const selectedColumns = collector.collect(node.returning); return selectedColumns.every((c) => idFields.includes(c)); } - private async enforcePreCreatePolicy( - node: InsertQueryNode, - proceed: ProceedKyselyQueryFunction - ) { + private async enforcePreCreatePolicy(node: InsertQueryNode, proceed: ProceedKyselyQueryFunction) { if (!node.columns || !node.values) { return; } const model = this.getMutationModel(node); const fields = node.columns.map((c) => c.column.name); - const valueRows = this.unwrapCreateValueRows( - node.values, - model, - fields - ); + const valueRows = this.unwrapCreateValueRows(node.values, model, fields); for (const values of valueRows) { await this.enforcePreCreatePolicyForOne( model, fields, values.map((v) => v.node), values.map((v) => v.raw), - proceed + proceed, ); } } @@ -203,7 +145,7 @@ export class PolicyHandler< fields: string[], values: OperationNode[], valuesRaw: unknown[], - proceed: ProceedKyselyQueryFunction + proceed: ProceedKyselyQueryFunction, ) { const thisEntity: Record = {}; const thisEntityRaw: Record = {}; @@ -212,23 +154,10 @@ export class PolicyHandler< thisEntityRaw[fields[i]!] = valuesRaw[i]!; } - const filter = this.buildPolicyFilter( - model, - undefined, - 'create', - thisEntity, - thisEntityRaw - ); + const filter = this.buildPolicyFilter(model, undefined, 'create', thisEntity, thisEntityRaw); const preCreateCheck: SelectQueryNode = { kind: 'SelectQueryNode', - selections: [ - SelectionNode.create( - AliasNode.create( - filter, - IdentifierNode.create('$condition') - ) - ), - ], + selections: [SelectionNode.create(AliasNode.create(filter, IdentifierNode.create('$condition')))], }; const result = await proceed(preCreateCheck); if (!(result.rows[0] as any)?.$condition) { @@ -236,62 +165,35 @@ export class PolicyHandler< } } - private unwrapCreateValueRows( - node: OperationNode, - model: GetModels, - fields: string[] - ) { + private unwrapCreateValueRows(node: OperationNode, model: GetModels, fields: string[]) { if (ValuesNode.is(node)) { - return node.values.map((v) => - this.unwrapCreateValueRow(v.values, model, fields) - ); + return node.values.map((v) => this.unwrapCreateValueRow(v.values, model, fields)); } else if (PrimitiveValueListNode.is(node)) { return [this.unwrapCreateValueRow(node.values, model, fields)]; } else { - throw new InternalError( - `Unexpected node kind: ${node.kind} for unwrapping create values` - ); + throw new InternalError(`Unexpected node kind: ${node.kind} for unwrapping create values`); } } - private unwrapCreateValueRow( - data: readonly unknown[], - model: GetModels, - fields: string[] - ) { - invariant( - data.length === fields.length, - 'data length must match fields length' - ); + private unwrapCreateValueRow(data: readonly unknown[], model: GetModels, fields: string[]) { + invariant(data.length === fields.length, 'data length must match fields length'); const result: { node: OperationNode; raw: unknown }[] = []; for (let i = 0; i < data.length; i++) { const item = data[i]!; - const fieldDef = requireField( - this.client.$schema, - model, - fields[i]! - ); + const fieldDef = requireField(this.client.$schema, model, fields[i]!); if (typeof item === 'object' && item && 'kind' in item) { invariant(item.kind === 'ValueNode', 'expecting a ValueNode'); result.push({ node: ValueNode.create( - this.dialect.transformPrimitive( - (item as ValueNode).value, - fieldDef.type as BuiltinType - ) + this.dialect.transformPrimitive((item as ValueNode).value, fieldDef.type as BuiltinType), ), raw: (item as ValueNode).value, }); } else { - const value = this.dialect.transformPrimitive( - item, - fieldDef.type as BuiltinType - ); + const value = this.dialect.transformPrimitive(item, fieldDef.type as BuiltinType); if (Array.isArray(value)) { result.push({ - node: RawNode.createWithSql( - this.dialect.buildArrayLiteralSQL(value) - ), + node: RawNode.createWithSql(this.dialect.buildArrayLiteralSQL(value)), raw: value, }); } else { @@ -302,27 +204,20 @@ export class PolicyHandler< return result; } - private tryGetConstantPolicy( - model: GetModels, - operation: PolicyOperation - ) { + private tryGetConstantPolicy(model: GetModels, operation: PolicyOperation) { const policies = this.getModelPolicies(model, operation); if (!policies.some((p) => p.kind === 'allow')) { // no allow -> unconditional deny return false; } else if ( // unconditional deny - policies.some( - (p) => p.kind === 'deny' && this.isTrueExpr(p.condition) - ) + policies.some((p) => p.kind === 'deny' && this.isTrueExpr(p.condition)) ) { return false; } else if ( // unconditional allow !policies.some((p) => p.kind === 'deny') && - policies.some( - (p) => p.kind === 'allow' && this.isTrueExpr(p.condition) - ) + policies.some((p) => p.kind === 'allow' && this.isTrueExpr(p.condition)) ) { return true; } else { @@ -334,11 +229,7 @@ export class PolicyHandler< return ExpressionUtils.isLiteral(expr) && expr.value === true; } - private async processReadBack( - node: CrudQueryNode, - result: QueryResult, - proceed: ProceedKyselyQueryFunction - ) { + private async processReadBack(node: CrudQueryNode, result: QueryResult, proceed: ProceedKyselyQueryFunction) { if (result.rows.length === 0) { return result; } @@ -350,9 +241,7 @@ export class PolicyHandler< // do a select (with policy) in place of returning const table = this.getMutationModel(node); if (!table) { - throw new InternalError( - `Unable to get table name for query node: ${node}` - ); + throw new InternalError(`Unable to get table name for query node: ${node}`); } const idConditions = this.buildIdConditions(table, result.rows); @@ -361,9 +250,7 @@ export class PolicyHandler< const select: SelectQueryNode = { kind: 'SelectQueryNode', from: FromNode.create([TableNode.create(table)]), - where: WhereNode.create( - conjunction(this.dialect, [idConditions, policyFilter]) - ), + where: WhereNode.create(conjunction(this.dialect, [idConditions, policyFilter])), selections: node.returning.selections, }; const selectResult = await proceed(select); @@ -381,60 +268,39 @@ export class PolicyHandler< BinaryOperationNode.create( ColumnNode.create(field), OperatorNode.create('='), - ValueNode.create(row[field]) - ) - ) - ) - ) + ValueNode.create(row[field]), + ), + ), + ), + ), ); } - private getMutationModel( - node: InsertQueryNode | UpdateQueryNode | DeleteQueryNode - ) { + private getMutationModel(node: InsertQueryNode | UpdateQueryNode | DeleteQueryNode) { const r = match(node) - .when( - InsertQueryNode.is, - (node) => getTableName(node.into) as GetModels - ) - .when( - UpdateQueryNode.is, - (node) => getTableName(node.table) as GetModels - ) + .when(InsertQueryNode.is, (node) => getTableName(node.into) as GetModels) + .when(UpdateQueryNode.is, (node) => getTableName(node.table) as GetModels) .when(DeleteQueryNode.is, (node) => { if (node.from.froms.length !== 1) { - throw new InternalError( - 'Only one from table is supported for delete' - ); + throw new InternalError('Only one from table is supported for delete'); } return getTableName(node.from.froms[0]) as GetModels; }) .exhaustive(); if (!r) { - throw new InternalError( - `Unable to get table name for query node: ${node}` - ); + throw new InternalError(`Unable to get table name for query node: ${node}`); } return r; } private isCrudQueryNode(node: RootOperationNode): node is CrudQueryNode { return ( - SelectQueryNode.is(node) || - InsertQueryNode.is(node) || - UpdateQueryNode.is(node) || - DeleteQueryNode.is(node) + SelectQueryNode.is(node) || InsertQueryNode.is(node) || UpdateQueryNode.is(node) || DeleteQueryNode.is(node) ); } - private isMutationQueryNode( - node: RootOperationNode - ): node is MutationQueryNode { - return ( - InsertQueryNode.is(node) || - UpdateQueryNode.is(node) || - DeleteQueryNode.is(node) - ); + private isMutationQueryNode(node: RootOperationNode): node is MutationQueryNode { + return InsertQueryNode.is(node) || UpdateQueryNode.is(node) || DeleteQueryNode.is(node); } private buildPolicyFilter( @@ -442,7 +308,7 @@ export class PolicyHandler< alias: string | undefined, operation: CRUD, thisEntity?: Record, - thisEntityRaw?: Record + thisEntityRaw?: Record, ) { const policies = this.getModelPolicies(model, operation); if (policies.length === 0) { @@ -451,29 +317,11 @@ export class PolicyHandler< const allows = policies .filter((policy) => policy.kind === 'allow') - .map((policy) => - this.transformPolicyCondition( - model, - alias, - operation, - policy, - thisEntity, - thisEntityRaw - ) - ); + .map((policy) => this.transformPolicyCondition(model, alias, operation, policy, thisEntity, thisEntityRaw)); const denies = policies .filter((policy) => policy.kind === 'deny') - .map((policy) => - this.transformPolicyCondition( - model, - alias, - operation, - policy, - thisEntity, - thisEntityRaw - ) - ); + .map((policy) => this.transformPolicyCondition(model, alias, operation, policy, thisEntity, thisEntityRaw)); let combinedPolicy: OperationNode; @@ -488,13 +336,10 @@ export class PolicyHandler< if (denies.length !== 0) { const combinedDenies = conjunction( this.dialect, - denies.map((d) => buildIsFalse(d, this.dialect)) + denies.map((d) => buildIsFalse(d, this.dialect)), ); // or(...allows) && and(...!denies) - combinedPolicy = conjunction(this.dialect, [ - combinedPolicy, - combinedDenies, - ]); + combinedPolicy = conjunction(this.dialect, [combinedPolicy, combinedDenies]); } } return combinedPolicy; @@ -509,9 +354,7 @@ export class PolicyHandler< const { model, alias } = extractResult; const filter = this.buildPolicyFilter(model, alias, 'read'); whereNode = WhereNode.create( - whereNode?.where - ? conjunction(this.dialect, [whereNode.where, filter]) - : filter + whereNode?.where ? conjunction(this.dialect, [whereNode.where, filter]) : filter, ); } }); @@ -536,16 +379,11 @@ export class PolicyHandler< return result; } else { // only return ID fields, that's enough for reading back the inserted row - const idFields = getIdFields( - this.client.$schema, - this.getMutationModel(node) - ); + const idFields = getIdFields(this.client.$schema, this.getMutationModel(node)); return { ...result, returning: ReturningNode.create( - idFields.map((field) => - SelectionNode.create(ColumnNode.create(field)) - ) + idFields.map((field) => SelectionNode.create(ColumnNode.create(field))), ), }; } @@ -554,42 +392,24 @@ export class PolicyHandler< protected override transformUpdateQuery(node: UpdateQueryNode) { const result = super.transformUpdateQuery(node); const mutationModel = this.getMutationModel(node); - const filter = this.buildPolicyFilter( - mutationModel, - undefined, - 'update' - ); + const filter = this.buildPolicyFilter(mutationModel, undefined, 'update'); return { ...result, - where: WhereNode.create( - result.where - ? conjunction(this.dialect, [result.where.where, filter]) - : filter - ), + where: WhereNode.create(result.where ? conjunction(this.dialect, [result.where.where, filter]) : filter), }; } protected override transformDeleteQuery(node: DeleteQueryNode) { const result = super.transformDeleteQuery(node); const mutationModel = this.getMutationModel(node); - const filter = this.buildPolicyFilter( - mutationModel, - undefined, - 'delete' - ); + const filter = this.buildPolicyFilter(mutationModel, undefined, 'delete'); return { ...result, - where: WhereNode.create( - result.where - ? conjunction(this.dialect, [result.where.where, filter]) - : filter - ), + where: WhereNode.create(result.where ? conjunction(this.dialect, [result.where.where, filter]) : filter), }; } - private extractTableName( - from: OperationNode - ): { model: GetModels; alias?: string } | undefined { + private extractTableName(from: OperationNode): { model: GetModels; alias?: string } | undefined { if (TableNode.is(from)) { return { model: from.table.identifier.name as GetModels }; } @@ -600,9 +420,7 @@ export class PolicyHandler< } return { model: inner.model, - alias: IdentifierNode.is(from.alias) - ? from.alias.name - : undefined, + alias: IdentifierNode.is(from.alias) ? from.alias.name : undefined, }; } else { // this can happen for subqueries, which will be handled when nested @@ -617,20 +435,19 @@ export class PolicyHandler< operation: CRUD, policy: Policy, thisEntity?: Record, - thisEntityRaw?: Record + thisEntityRaw?: Record, ) { - return new ExpressionTransformer( - this.client.$schema, - this.client.$options, - this.client.$auth - ).transform(policy.condition, { - model, - alias, - operation, - thisEntity, - thisEntityRaw, - auth: this.client.$auth, - }); + return new ExpressionTransformer(this.client.$schema, this.client.$options, this.client.$auth).transform( + policy.condition, + { + model, + alias, + operation, + thisEntity, + thisEntityRaw, + auth: this.client.$auth, + }, + ); } private getModelPolicies(modelName: string, operation: PolicyOperation) { @@ -639,10 +456,7 @@ export class PolicyHandler< const extractOperations = (expr: Expression) => { invariant(ExpressionUtils.isLiteral(expr), 'expecting a literal'); - invariant( - typeof expr.value === 'string', - 'expecting a string literal' - ); + invariant(typeof expr.value === 'string', 'expecting a string literal'); return expr.value .split(',') .filter((v) => !!v) @@ -652,26 +466,16 @@ export class PolicyHandler< if (modelDef.attributes) { result.push( ...modelDef.attributes - .filter( - (attr) => - attr.name === '@@allow' || attr.name === '@@deny' - ) + .filter((attr) => attr.name === '@@allow' || attr.name === '@@deny') .map( (attr) => ({ - kind: - attr.name === '@@allow' ? 'allow' : 'deny', - operations: extractOperations( - attr.args![0]!.value - ), + kind: attr.name === '@@allow' ? 'allow' : 'deny', + operations: extractOperations(attr.args![0]!.value), condition: attr.args![1]!.value, - } as const) - ) - .filter( - (policy) => - policy.operations.includes('all') || - policy.operations.includes(operation) + }) as const, ) + .filter((policy) => policy.operations.includes('all') || policy.operations.includes(operation)), ); } return result; diff --git a/packages/runtime/src/plugins/policy/utils.ts b/packages/runtime/src/plugins/policy/utils.ts index 7b689641..01960533 100644 --- a/packages/runtime/src/plugins/policy/utils.ts +++ b/packages/runtime/src/plugins/policy/utils.ts @@ -18,23 +18,15 @@ import type { SchemaDef } from '../../schema'; /** * Creates a `true` value node. */ -export function trueNode( - dialect: BaseCrudDialect -) { - return ValueNode.createImmediate( - dialect.transformPrimitive(true, 'Boolean') - ); +export function trueNode(dialect: BaseCrudDialect) { + return ValueNode.createImmediate(dialect.transformPrimitive(true, 'Boolean')); } /** * Creates a `false` value node. */ -export function falseNode( - dialect: BaseCrudDialect -) { - return ValueNode.createImmediate( - dialect.transformPrimitive(false, 'Boolean') - ); +export function falseNode(dialect: BaseCrudDialect) { + return ValueNode.createImmediate(dialect.transformPrimitive(false, 'Boolean')); } /** @@ -56,7 +48,7 @@ export function isFalseNode(node: OperationNode): boolean { */ export function conjunction( dialect: BaseCrudDialect, - nodes: OperationNode[] + nodes: OperationNode[], ): OperationNode { if (nodes.some(isFalseNode)) { return falseNode(dialect); @@ -68,13 +60,13 @@ export function conjunction( return items.reduce((acc, node) => OrNode.is(node) ? AndNode.create(acc, ParensNode.create(node)) // wraps parentheses - : AndNode.create(acc, node) + : AndNode.create(acc, node), ); } export function disjunction( dialect: BaseCrudDialect, - nodes: OperationNode[] + nodes: OperationNode[], ): OperationNode { if (nodes.some(isTrueNode)) { return trueNode(dialect); @@ -86,7 +78,7 @@ export function disjunction( return items.reduce((acc, node) => AndNode.is(node) ? OrNode.create(acc, ParensNode.create(node)) // wraps parentheses - : OrNode.create(acc, node) + : OrNode.create(acc, node), ); } @@ -98,36 +90,26 @@ export function logicalNot(node: OperationNode): OperationNode { OperatorNode.create('not'), AndNode.is(node) || OrNode.is(node) ? ParensNode.create(node) // wraps parentheses - : node + : node, ); } /** * Builds an expression node that checks if a node is true. */ -export function buildIsTrue( - node: OperationNode, - dialect: BaseCrudDialect -) { +export function buildIsTrue(node: OperationNode, dialect: BaseCrudDialect) { if (isTrueNode(node)) { return trueNode(dialect); } else if (isFalseNode(node)) { return falseNode(dialect); } - return BinaryOperationNode.create( - node, - OperatorNode.create('='), - trueNode(dialect) - ); + return BinaryOperationNode.create(node, OperatorNode.create('='), trueNode(dialect)); } /** * Builds an expression node that checks if a node is false. */ -export function buildIsFalse( - node: OperationNode, - dialect: BaseCrudDialect -) { +export function buildIsFalse(node: OperationNode, dialect: BaseCrudDialect) { if (isFalseNode(node)) { return trueNode(dialect); } else if (isTrueNode(node)) { @@ -137,7 +119,7 @@ export function buildIsFalse( // coalesce so null is treated as false FunctionNode.create('coalesce', [node, falseNode(dialect)]), OperatorNode.create('='), - falseNode(dialect) + falseNode(dialect), ); } diff --git a/packages/runtime/src/schema/auth.ts b/packages/runtime/src/schema/auth.ts index 7e47a778..868025ff 100644 --- a/packages/runtime/src/schema/auth.ts +++ b/packages/runtime/src/schema/auth.ts @@ -5,5 +5,5 @@ export type AuthType = string extends GetModels ? Record : Schema['authType'] extends GetModels - ? Partial> - : never; + ? Partial> + : never; diff --git a/packages/runtime/src/schema/expression.ts b/packages/runtime/src/schema/expression.ts index cf490835..6ae1c158 100644 --- a/packages/runtime/src/schema/expression.ts +++ b/packages/runtime/src/schema/expression.ts @@ -36,11 +36,7 @@ export const ExpressionUtils = { }; }, - binary: ( - left: Expression, - op: BinaryOperator, - right: Expression - ): BinaryExpression => { + binary: (left: Expression, op: BinaryOperator, right: Expression): BinaryExpression => { return { kind: 'binary', op, @@ -85,52 +81,32 @@ export const ExpressionUtils = { }, and: (expr: Expression, ...expressions: Expression[]) => { - return expressions.reduce( - (acc, exp) => ExpressionUtils.binary(acc, '&&', exp), - expr - ); + return expressions.reduce((acc, exp) => ExpressionUtils.binary(acc, '&&', exp), expr); }, or: (expr: Expression, ...expressions: Expression[]) => { - return expressions.reduce( - (acc, exp) => ExpressionUtils.binary(acc, '||', exp), - expr - ); + return expressions.reduce((acc, exp) => ExpressionUtils.binary(acc, '||', exp), expr); }, is: (value: unknown, kind: Expression['kind']): value is Expression => { - return ( - !!value && - typeof value === 'object' && - 'kind' in value && - value.kind === kind - ); + return !!value && typeof value === 'object' && 'kind' in value && value.kind === kind; }, - isLiteral: (value: unknown): value is LiteralExpression => - ExpressionUtils.is(value, 'literal'), + isLiteral: (value: unknown): value is LiteralExpression => ExpressionUtils.is(value, 'literal'), - isArray: (value: unknown): value is ArrayExpression => - ExpressionUtils.is(value, 'array'), + isArray: (value: unknown): value is ArrayExpression => ExpressionUtils.is(value, 'array'), - isCall: (value: unknown): value is CallExpression => - ExpressionUtils.is(value, 'call'), + isCall: (value: unknown): value is CallExpression => ExpressionUtils.is(value, 'call'), - isNull: (value: unknown): value is NullExpression => - ExpressionUtils.is(value, 'null'), + isNull: (value: unknown): value is NullExpression => ExpressionUtils.is(value, 'null'), - isThis: (value: unknown): value is ThisExpression => - ExpressionUtils.is(value, 'this'), + isThis: (value: unknown): value is ThisExpression => ExpressionUtils.is(value, 'this'), - isUnary: (value: unknown): value is UnaryExpression => - ExpressionUtils.is(value, 'unary'), + isUnary: (value: unknown): value is UnaryExpression => ExpressionUtils.is(value, 'unary'), - isBinary: (value: unknown): value is BinaryExpression => - ExpressionUtils.is(value, 'binary'), + isBinary: (value: unknown): value is BinaryExpression => ExpressionUtils.is(value, 'binary'), - isField: (value: unknown): value is FieldExpression => - ExpressionUtils.is(value, 'field'), + isField: (value: unknown): value is FieldExpression => ExpressionUtils.is(value, 'field'), - isMember: (value: unknown): value is MemberExpression => - ExpressionUtils.is(value, 'member'), + isMember: (value: unknown): value is MemberExpression => ExpressionUtils.is(value, 'member'), }; diff --git a/packages/runtime/src/utils/clone.ts b/packages/runtime/src/utils/clone.ts index aaf735fa..82355f6a 100644 --- a/packages/runtime/src/utils/clone.ts +++ b/packages/runtime/src/utils/clone.ts @@ -14,7 +14,7 @@ export function clone(value: T): T { return value; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any = {}; for (const key of Object.keys(value)) { result[key] = clone(value[key as keyof T]); diff --git a/packages/runtime/src/utils/default-operation-node-visitor.ts b/packages/runtime/src/utils/default-operation-node-visitor.ts index 8881b0ee..404b33a1 100644 --- a/packages/runtime/src/utils/default-operation-node-visitor.ts +++ b/packages/runtime/src/utils/default-operation-node-visitor.ts @@ -105,11 +105,7 @@ export class DefaultOperationNodeVisitor extends OperationNodeVisitor { if (Array.isArray(value)) { value.forEach((el) => this.defaultVisit(el)); } - if ( - typeof value === 'object' && - 'kind' in value && - typeof value.kind === 'string' - ) { + if (typeof value === 'object' && 'kind' in value && typeof value.kind === 'string') { this.visitNode(value); } }); @@ -220,17 +216,13 @@ export class DefaultOperationNodeVisitor extends OperationNodeVisitor { protected override visitIdentifier(node: IdentifierNode): void { this.defaultVisit(node); } - protected override visitSchemableIdentifier( - node: SchemableIdentifierNode - ): void { + protected override visitSchemableIdentifier(node: SchemableIdentifierNode): void { this.defaultVisit(node); } protected override visitValue(node: ValueNode): void { this.defaultVisit(node); } - protected override visitPrimitiveValueList( - node: PrimitiveValueListNode - ): void { + protected override visitPrimitiveValueList(node: PrimitiveValueListNode): void { this.defaultVisit(node); } protected override visitOperator(node: OperatorNode): void { @@ -245,9 +237,7 @@ export class DefaultOperationNodeVisitor extends OperationNodeVisitor { protected override visitList(node: ListNode): void { this.defaultVisit(node); } - protected override visitPrimaryKeyConstraint( - node: PrimaryKeyConstraintNode - ): void { + protected override visitPrimaryKeyConstraint(node: PrimaryKeyConstraintNode): void { this.defaultVisit(node); } protected override visitUniqueConstraint(node: UniqueConstraintNode): void { @@ -259,14 +249,10 @@ export class DefaultOperationNodeVisitor extends OperationNodeVisitor { protected override visitWith(node: WithNode): void { this.defaultVisit(node); } - protected override visitCommonTableExpression( - node: CommonTableExpressionNode - ): void { + protected override visitCommonTableExpression(node: CommonTableExpressionNode): void { this.defaultVisit(node); } - protected override visitCommonTableExpressionName( - node: CommonTableExpressionNameNode - ): void { + protected override visitCommonTableExpressionName(node: CommonTableExpressionNameNode): void { this.defaultVisit(node); } protected override visitHaving(node: HavingNode): void { @@ -299,9 +285,7 @@ export class DefaultOperationNodeVisitor extends OperationNodeVisitor { protected override visitDropConstraint(node: DropConstraintNode): void { this.defaultVisit(node); } - protected override visitForeignKeyConstraint( - node: ForeignKeyConstraintNode - ): void { + protected override visitForeignKeyConstraint(node: ForeignKeyConstraintNode): void { this.defaultVisit(node); } protected override visitCreateView(node: CreateViewNode): void { @@ -334,14 +318,10 @@ export class DefaultOperationNodeVisitor extends OperationNodeVisitor { protected override visitExplain(node: ExplainNode): void { this.defaultVisit(node); } - protected override visitDefaultInsertValue( - node: DefaultInsertValueNode - ): void { + protected override visitDefaultInsertValue(node: DefaultInsertValueNode): void { this.defaultVisit(node); } - protected override visitAggregateFunction( - node: AggregateFunctionNode - ): void { + protected override visitAggregateFunction(node: AggregateFunctionNode): void { this.defaultVisit(node); } protected override visitOver(node: OverNode): void { @@ -383,9 +363,7 @@ export class DefaultOperationNodeVisitor extends OperationNodeVisitor { protected override visitJSONPathLeg(node: JSONPathLegNode): void { this.defaultVisit(node); } - protected override visitJSONOperatorChain( - node: JSONOperatorChainNode - ): void { + protected override visitJSONOperatorChain(node: JSONOperatorChainNode): void { this.defaultVisit(node); } protected override visitTuple(node: TupleNode): void { diff --git a/packages/runtime/src/utils/object-utils.ts b/packages/runtime/src/utils/object-utils.ts index 59a2346a..e05ebe5d 100644 --- a/packages/runtime/src/utils/object-utils.ts +++ b/packages/runtime/src/utils/object-utils.ts @@ -2,16 +2,12 @@ * Extract fields from an object. */ export function extractFields(obj: any, fields: string[]) { - return Object.fromEntries( - Object.entries(obj).filter(([key]) => fields.includes(key)) - ); + return Object.fromEntries(Object.entries(obj).filter(([key]) => fields.includes(key))); } /** * Create an object with fields as keys and true values. */ -export function fieldsToSelectObject( - fields: string[] -): Record { +export function fieldsToSelectObject(fields: string[]): Record { return Object.fromEntries(fields.map((f) => [f, true])); } diff --git a/packages/runtime/src/utils/type-utils.ts b/packages/runtime/src/utils/type-utils.ts index a8468d31..0046b429 100644 --- a/packages/runtime/src/utils/type-utils.ts +++ b/packages/runtime/src/utils/type-utils.ts @@ -1,56 +1,43 @@ import type Decimal from 'decimal.js'; -export type NullableIf = Condition extends true - ? T | null - : T; +export type NullableIf = Condition extends true ? T | null : T; -export type PartialRecord = Partial< - Record ->; +export type PartialRecord = Partial>; export type WrapType = Optional extends true ? T | null : Array extends true - ? T[] - : T; + ? T[] + : T; export type MapBaseType = T extends 'String' ? string : T extends 'Boolean' - ? boolean - : T extends 'Int' | 'Float' - ? number - : T extends 'BigInt' - ? bigint - : T extends 'Decimal' - ? Decimal - : T extends 'DateTime' - ? Date - : T extends 'Json' - ? JsonValue - : unknown; - -export type JsonValue = - | string - | number - | boolean - | null - | JsonObject - | JsonArray; + ? boolean + : T extends 'Int' | 'Float' + ? number + : T extends 'BigInt' + ? bigint + : T extends 'Decimal' + ? Decimal + : T extends 'DateTime' + ? Date + : T extends 'Json' + ? JsonValue + : unknown; + +export type JsonValue = string | number | boolean | null | JsonObject | JsonArray; export type JsonObject = { [key: string]: JsonValue }; export type JsonArray = Array; -// eslint-disable-next-line @typescript-eslint/ban-types export type Simplify = { [Key in keyof T]: T[Key] } & {}; export function call(code: string) { return { code }; } -export type OrArray = IF extends true - ? T | T[] - : T; +export type OrArray = IF extends true ? T | T[] : T; export type NonEmptyArray = [T, ...T[]]; @@ -62,32 +49,20 @@ type NoExpand = T extends unknown ? T : never; // this type assumes the passed object is entirely optional export type AtLeast = NoExpand< O extends unknown - ? - | (K extends keyof O ? { [P in K]: O[P] } & O : O) - | ({ [P in keyof O as P extends K ? K : never]-?: O[P] } & O) + ? (K extends keyof O ? { [P in K]: O[P] } & O : O) | ({ [P in keyof O as P extends K ? K : never]-?: O[P] } & O) : never >; type Without = { [P in Exclude]?: never }; -export type XOR = T extends object - ? U extends object - ? (Without & U) | (Without & T) - : U - : T; +export type XOR = T extends object ? (U extends object ? (Without & U) | (Without & T) : U) : T; -export type MergeIf = Condition extends true - ? T & U - : T; +export type MergeIf = Condition extends true ? T & U : T; export type MaybePromise = T | Promise; -export type PrependParameter = Func extends ( - ...args: any[] -) => infer R +export type PrependParameter = Func extends (...args: any[]) => infer R ? (p: Param, ...args: Parameters) => R : never; -export type OrUndefinedIf = Condition extends true - ? T | undefined - : T; +export type OrUndefinedIf = Condition extends true ? T | undefined : T; diff --git a/packages/runtime/test/client-api/aggregate.test.ts b/packages/runtime/test/client-api/aggregate.test.ts index 943f2ce1..baf5b8e7 100644 --- a/packages/runtime/test/client-api/aggregate.test.ts +++ b/packages/runtime/test/client-api/aggregate.test.ts @@ -6,200 +6,197 @@ import { createUser } from './utils'; const PG_DB_NAME = 'client-api-aggregate-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client aggregate tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client aggregate tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); + beforeEach(async () => { + client = await createClient(); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); + + it('works with count', async () => { + await createUser(client, 'u1@test.com', { name: 'User1' }); + await createUser(client, 'u2@test.com', { name: null }); + + // count + const r1 = await client.user.aggregate({ + _count: true, }); + expect(r1._count).toBe(2); - afterEach(async () => { - await client?.$disconnect(); + const r2 = await client.user.aggregate({ + _count: { _all: true, name: true }, }); + expect(r2._count._all).toBe(2); + expect(r2._count.name).toBe(1); + }); - it('works with count', async () => { - await createUser(client, 'u1@test.com', { name: 'User1' }); - await createUser(client, 'u2@test.com', { name: null }); + it('works with filter', async () => { + await createUser(client, 'u1@test.com', { name: 'User1' }); + await createUser(client, 'u2@test.com', { name: null }); - // count - const r1 = await client.user.aggregate({ + await expect( + client.user.aggregate({ _count: true, - }); - expect(r1._count).toBe(2); - - const r2 = await client.user.aggregate({ - _count: { _all: true, name: true }, - }); - expect(r2._count._all).toBe(2); - expect(r2._count.name).toBe(1); - }); + where: { email: { contains: 'u1' } }, + }), + ).resolves.toMatchObject({ _count: 1 }); + }); - it('works with filter', async () => { - await createUser(client, 'u1@test.com', { name: 'User1' }); - await createUser(client, 'u2@test.com', { name: null }); + it('works with skip, take, orderBy', async () => { + await createUser(client, 'u1@test.com', { name: 'User1' }); + await createUser(client, 'u2@test.com', { name: 'User2' }); + await createUser(client, 'u3@test.com', { name: 'User3' }); - await expect( - client.user.aggregate({ - _count: true, - where: { email: { contains: 'u1' } }, - }) - ).resolves.toMatchObject({ _count: 1 }); + await expect( + client.user.aggregate({ + _count: true, + skip: 1, + take: 1, + }), + ).resolves.toMatchObject({ _count: 1 }); + + await expect( + client.user.aggregate({ + _count: true, + orderBy: { name: 'asc' }, + }), + ).resolves.toMatchObject({ _count: 3 }); + + await expect( + client.user.aggregate({ + _count: true, + take: -2, + }), + ).resolves.toMatchObject({ _count: 2 }); + }); + + it('works with sum and avg', async () => { + await client.profile.create({ data: { age: 10, bio: 'Bio1' } }); + await client.profile.create({ data: { age: 20, bio: 'Bio2' } }); + await expect( + client.profile.aggregate({ + _avg: { age: true }, + _sum: { age: true }, + }), + ).resolves.toMatchObject({ + _avg: { age: 15 }, + _sum: { age: 30 }, }); - it('works with skip, take, orderBy', async () => { - await createUser(client, 'u1@test.com', { name: 'User1' }); - await createUser(client, 'u2@test.com', { name: 'User2' }); - await createUser(client, 'u3@test.com', { name: 'User3' }); - - await expect( - client.user.aggregate({ - _count: true, - skip: 1, - take: 1, - }) - ).resolves.toMatchObject({ _count: 1 }); - - await expect( - client.user.aggregate({ - _count: true, - orderBy: { name: 'asc' }, - }) - ).resolves.toMatchObject({ _count: 3 }); - - await expect( - client.user.aggregate({ - _count: true, - take: -2, - }) - ).resolves.toMatchObject({ _count: 2 }); + client.user.aggregate({ + // @ts-expect-error + _sum: { name: true }, + }); + }); + + it('works with min and max', async () => { + await client.profile.create({ data: { age: 10, bio: 'Bio1' } }); + await client.profile.create({ data: { age: 20, bio: 'Bio2' } }); + const r = await client.profile.aggregate({ + _min: { age: true, bio: true }, + _max: { age: true, bio: true }, }); - it('works with sum and avg', async () => { - await client.profile.create({ data: { age: 10, bio: 'Bio1' } }); - await client.profile.create({ data: { age: 20, bio: 'Bio2' } }); - await expect( - client.profile.aggregate({ - _avg: { age: true }, - _sum: { age: true }, - }) - ).resolves.toMatchObject({ - _avg: { age: 15 }, - _sum: { age: 30 }, - }); + expect(r._min.age).toBe(10); + expect(r._max.age).toBe(20); + expect(r._min.bio).toBe('Bio1'); + expect(r._max.bio).toBe('Bio2'); + }); - client.user.aggregate({ - // @ts-expect-error - _sum: { name: true }, - }); + it('works with scalar orderBy', async () => { + await createUser(client, 'u1@test.com', { + name: 'Admin', + role: 'ADMIN', }); - - it('works with min and max', async () => { - await client.profile.create({ data: { age: 10, bio: 'Bio1' } }); - await client.profile.create({ data: { age: 20, bio: 'Bio2' } }); - const r = await client.profile.aggregate({ - _min: { age: true, bio: true }, - _max: { age: true, bio: true }, - }); - - expect(r._min.age).toBe(10); - expect(r._max.age).toBe(20); - expect(r._min.bio).toBe('Bio1'); - expect(r._max.bio).toBe('Bio2'); + await createUser(client, 'u2@test.com', { + name: 'User', + role: 'USER', + }); + await createUser(client, 'u3@test.com', { + name: null, + role: 'USER', }); - it('works with scalar orderBy', async () => { - await createUser(client, 'u1@test.com', { - name: 'Admin', - role: 'ADMIN', - }); - await createUser(client, 'u2@test.com', { - name: 'User', - role: 'USER', - }); - await createUser(client, 'u3@test.com', { - name: null, - role: 'USER', - }); - - await expect( - client.user.aggregate({ - orderBy: { - role: 'desc', - }, - take: 2, - _count: { - name: true, - }, - }) - ).resolves.toMatchObject({ - _count: { - name: 1, + await expect( + client.user.aggregate({ + orderBy: { + role: 'desc', }, - }); - - await expect( - client.user.aggregate({ - orderBy: { - name: { sort: 'asc', nulls: 'last' }, - }, - take: 2, - _count: { - name: true, - }, - }) - ).resolves.toMatchObject({ + take: 2, _count: { - name: 2, + name: true, }, - }); + }), + ).resolves.toMatchObject({ + _count: { + name: 1, + }, }); - it('works with relation orderBy', async () => { - await createUser(client, 'u1@test.com', { - name: 'Admin', - role: 'ADMIN', - profile: { create: { bio: 'bio', age: 10 } }, - }); - await createUser(client, 'u2@test.com', { - name: 'User', - role: 'USER', - profile: { create: { bio: 'bio', age: 20 } }, - }); - await createUser(client, 'u3@test.com', { - name: null, - role: 'USER', - profile: { create: { bio: 'bio', age: 30 } }, - }); - - await expect( - client.user.aggregate({ - take: 2, - orderBy: { - profile: { age: 'asc' }, - }, - _count: { name: true }, - }) - ).resolves.toMatchObject({ - _count: { - name: 2, + await expect( + client.user.aggregate({ + orderBy: { + name: { sort: 'asc', nulls: 'last' }, }, - }); - - await expect( - client.user.aggregate({ - take: 2, - orderBy: { - profile: { age: 'desc' }, - }, - _count: { name: true }, - }) - ).resolves.toMatchObject({ + take: 2, _count: { - name: 1, + name: true, + }, + }), + ).resolves.toMatchObject({ + _count: { + name: 2, + }, + }); + }); + + it('works with relation orderBy', async () => { + await createUser(client, 'u1@test.com', { + name: 'Admin', + role: 'ADMIN', + profile: { create: { bio: 'bio', age: 10 } }, + }); + await createUser(client, 'u2@test.com', { + name: 'User', + role: 'USER', + profile: { create: { bio: 'bio', age: 20 } }, + }); + await createUser(client, 'u3@test.com', { + name: null, + role: 'USER', + profile: { create: { bio: 'bio', age: 30 } }, + }); + + await expect( + client.user.aggregate({ + take: 2, + orderBy: { + profile: { age: 'asc' }, + }, + _count: { name: true }, + }), + ).resolves.toMatchObject({ + _count: { + name: 2, + }, + }); + + await expect( + client.user.aggregate({ + take: 2, + orderBy: { + profile: { age: 'desc' }, }, - }); + _count: { name: true }, + }), + ).resolves.toMatchObject({ + _count: { + name: 1, + }, }); - } -); + }); +}); diff --git a/packages/runtime/test/client-api/client-specs.ts b/packages/runtime/test/client-api/client-specs.ts index de2296b2..f05c5fcd 100644 --- a/packages/runtime/test/client-api/client-specs.ts +++ b/packages/runtime/test/client-api/client-specs.ts @@ -3,18 +3,10 @@ import { getSchema, schema } from '../test-schema'; import { makePostgresClient, makeSqliteClient } from '../utils'; import type { ClientContract } from '../../src'; -export function createClientSpecs( - dbName: string, - logQueries = false, - providers = ['sqlite', 'postgresql'] as const -) { +export function createClientSpecs(dbName: string, logQueries = false, providers = ['sqlite', 'postgresql'] as const) { const logger = (provider: string) => (event: LogEvent) => { if (event.level === 'query') { - console.log( - `query(${provider}):`, - event.query.sql, - event.query.parameters - ); + console.log(`query(${provider}):`, event.query.sql, event.query.parameters); } }; return [ @@ -24,14 +16,9 @@ export function createClientSpecs( provider: 'sqlite' as const, schema: getSchema('sqlite'), createClient: async () => { - const client = await makeSqliteClient( - getSchema('sqlite'), - { - log: logQueries - ? logger('sqlite') - : undefined, - } - ); + const client = await makeSqliteClient(getSchema('sqlite'), { + log: logQueries ? logger('sqlite') : undefined, + }); return client as ClientContract; }, }, @@ -43,18 +30,10 @@ export function createClientSpecs( provider: 'postgresql' as const, schema: getSchema('postgresql'), createClient: async () => { - const client = await makePostgresClient( - getSchema('postgresql'), - dbName, - { - log: logQueries - ? logger('postgresql') - : undefined, - } - ); - return client as unknown as ClientContract< - typeof schema - >; + const client = await makePostgresClient(getSchema('postgresql'), dbName, { + log: logQueries ? logger('postgresql') : undefined, + }); + return client as unknown as ClientContract; }, }, ] diff --git a/packages/runtime/test/client-api/compound-id.test.ts b/packages/runtime/test/client-api/compound-id.test.ts index f1e0abe6..88457467 100644 --- a/packages/runtime/test/client-api/compound-id.test.ts +++ b/packages/runtime/test/client-api/compound-id.test.ts @@ -30,7 +30,7 @@ describe('Compound ID tests', () => { id2: 1, name: 'User1', }, - }) + }), ).resolves.toMatchObject({ id1: 1, id2: 1, @@ -46,7 +46,7 @@ describe('Compound ID tests', () => { connect: { id1_id2: { id1: 1, id2: 2 } }, }, }, - }) + }), ).toBeRejectedNotFound(); await expect( @@ -58,7 +58,7 @@ describe('Compound ID tests', () => { connect: { id1_id2: { id1: 1, id2: 1 } }, }, }, - }) + }), ).resolves.toMatchObject({ authorId1: 1, authorId2: 1, @@ -90,7 +90,7 @@ describe('Compound ID tests', () => { id2: 2, }, }, - }) + }), ).toResolveNull(); await expect( @@ -101,7 +101,7 @@ describe('Compound ID tests', () => { id2: 1, }, }, - }) + }), ).toResolveTruthy(); await expect( @@ -109,7 +109,7 @@ describe('Compound ID tests', () => { where: { id1: 1, }, - }) + }), ).rejects.toThrow(/id1_id2/); }); @@ -125,7 +125,7 @@ describe('Compound ID tests', () => { client.user.update({ where: { id1_id2: { id1: 1, id2: 1 } }, data: { name: 'User1-1' }, - }) + }), ).resolves.toMatchObject({ name: 'User1-1' }); // toplevel, not found @@ -133,7 +133,7 @@ describe('Compound ID tests', () => { client.user.update({ where: { id1_id2: { id1: 1, id2: 1 }, id1: 2 }, data: { name: 'User1-1' }, - }) + }), ).toBeRejectedNotFound(); await client.post.create({ @@ -152,7 +152,7 @@ describe('Compound ID tests', () => { connect: { id1_id2: { id1: 1, id2: 1 } }, }, }, - }) + }), ).resolves.toMatchObject({ authorId1: 1, authorId2: 1 }); // disconnect not found @@ -160,7 +160,7 @@ describe('Compound ID tests', () => { client.post.update({ where: { id: 1 }, data: { author: { disconnect: { id1: 1, id2: 2 } } }, - }) + }), ).resolves.toMatchObject({ authorId1: 1, authorId2: 1 }); // disconnect found @@ -168,7 +168,7 @@ describe('Compound ID tests', () => { client.post.update({ where: { id: 1 }, data: { author: { disconnect: { id1: 1, id2: 1 } } }, - }) + }), ).resolves.toMatchObject({ authorId1: null, authorId2: null }); // reconnect @@ -186,7 +186,7 @@ describe('Compound ID tests', () => { client.post.update({ where: { id: 1 }, data: { author: { disconnect: true } }, - }) + }), ).resolves.toMatchObject({ authorId1: null, authorId2: null }); // connectOrCreate - connect @@ -208,7 +208,7 @@ describe('Compound ID tests', () => { include: { author: true, }, - }) + }), ).resolves.toMatchObject({ author: { id1: 1, @@ -236,7 +236,7 @@ describe('Compound ID tests', () => { include: { author: true, }, - }) + }), ).resolves.toMatchObject({ author: { id1: 2, @@ -259,7 +259,7 @@ describe('Compound ID tests', () => { }, }, include: { author: true }, - }) + }), ).resolves.toMatchObject({ author: { name: 'User3' } }); // upsert - update @@ -276,7 +276,7 @@ describe('Compound ID tests', () => { }, }, include: { author: true }, - }) + }), ).resolves.toMatchObject({ author: { name: 'User3-1' } }); // delete, and post is cascade deleted @@ -284,7 +284,7 @@ describe('Compound ID tests', () => { client.post.update({ where: { id: 1 }, data: { author: { delete: true } }, - }) + }), ).toResolveNull(); // delete not found @@ -292,7 +292,7 @@ describe('Compound ID tests', () => { client.post.update({ where: { id: 1 }, data: { author: { delete: true } }, - }) + }), ).toBeRejectedNotFound(); }); @@ -305,7 +305,7 @@ describe('Compound ID tests', () => { where: { id1_id2: { id1: 1, id2: 1 } }, create: { id1: 1, id2: 1, name: 'User1' }, update: { name: 'User1-1' }, - }) + }), ).resolves.toMatchObject({ name: 'User1' }); // toplevel, update @@ -314,7 +314,7 @@ describe('Compound ID tests', () => { where: { id1_id2: { id1: 1, id2: 1 } }, create: { id1: 1, id2: 1, name: 'User1' }, update: { name: 'User1-1' }, - }) + }), ).resolves.toMatchObject({ name: 'User1-1' }); }); @@ -329,14 +329,14 @@ describe('Compound ID tests', () => { await expect( client.user.delete({ where: { id1_id2: { id1: 1, id2: 1 } }, - }) + }), ).resolves.toMatchObject({ name: 'User1' }); // toplevel await expect( client.user.delete({ where: { id1_id2: { id1: 1, id2: 1 } }, - }) + }), ).toBeRejectedNotFound(); }); }); @@ -377,7 +377,7 @@ describe('Compound ID tests', () => { posts: { connect: { id1_id2: { id1: 1, id2: 1 } } }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ posts: [expect.objectContaining({ id1: 1, id2: 1 })], }); @@ -390,7 +390,7 @@ describe('Compound ID tests', () => { posts: { connect: { id1_id2: { id1: 1, id2: 2 } } }, }, include: { posts: true }, - }) + }), ).toBeRejectedNotFound(); // connectOrCreate - connect @@ -411,7 +411,7 @@ describe('Compound ID tests', () => { }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ posts: [expect.objectContaining({ title: 'Post1' })], }); @@ -434,7 +434,7 @@ describe('Compound ID tests', () => { }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ posts: [expect.objectContaining({ title: 'Post2' })], }); @@ -464,7 +464,7 @@ describe('Compound ID tests', () => { data: { title: 'Post1-1', }, - }) + }), ).resolves.toMatchObject({ title: 'Post1-1' }); // create @@ -481,12 +481,9 @@ describe('Compound ID tests', () => { }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ - posts: [ - expect.objectContaining({ title: 'Post1-1' }), - expect.objectContaining({ title: 'Post2' }), - ], + posts: [expect.objectContaining({ title: 'Post1-1' }), expect.objectContaining({ title: 'Post2' })], }); // connect - not found @@ -499,7 +496,7 @@ describe('Compound ID tests', () => { }, }, include: { posts: true }, - }) + }), ).toBeRejectedNotFound(); await client.post.create({ @@ -520,7 +517,7 @@ describe('Compound ID tests', () => { }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ posts: [ expect.objectContaining({ title: 'Post1-1' }), @@ -538,7 +535,7 @@ describe('Compound ID tests', () => { disconnect: { id1: 1, id2: 1 }, }, }, - }) + }), ).rejects.toThrow(/Invalid/); // disconnect @@ -551,12 +548,9 @@ describe('Compound ID tests', () => { }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ - posts: [ - expect.objectContaining({ title: 'Post2' }), - expect.objectContaining({ title: 'Post3' }), - ], + posts: [expect.objectContaining({ title: 'Post2' }), expect.objectContaining({ title: 'Post3' })], }); // disconnect not found @@ -568,7 +562,7 @@ describe('Compound ID tests', () => { disconnect: { id1_id2: { id1: 10, id2: 10 } }, }, }, - }) + }), ).toResolveTruthy(); // update @@ -586,11 +580,9 @@ describe('Compound ID tests', () => { }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ - posts: expect.arrayContaining([ - expect.objectContaining({ title: 'Post2-new' }), - ]), + posts: expect.arrayContaining([expect.objectContaining({ title: 'Post2-new' })]), }); // delete @@ -603,7 +595,7 @@ describe('Compound ID tests', () => { }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ posts: expect.not.arrayContaining([{ title: 'Post3' }]), }); @@ -614,19 +606,13 @@ describe('Compound ID tests', () => { where: { id: 1 }, data: { posts: { - set: [ - { id1_id2: { id1: 1, id2: 1 } }, - { id1_id2: { id1: 2, id2: 2 } }, - ], + set: [{ id1_id2: { id1: 1, id2: 1 } }, { id1_id2: { id1: 2, id2: 2 } }], }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ - posts: [ - expect.objectContaining({ id1: 1, id2: 1 }), - expect.objectContaining({ id1: 2, id2: 2 }), - ], + posts: [expect.objectContaining({ id1: 1, id2: 1 }), expect.objectContaining({ id1: 2, id2: 2 })], }); }); @@ -639,7 +625,7 @@ describe('Compound ID tests', () => { where: { id1_id2: { id1: 1, id2: 1 } }, create: { id1: 1, id2: 1, title: 'Post1' }, update: { title: 'Post1-1' }, - }) + }), ).resolves.toMatchObject({ title: 'Post1' }); // update @@ -648,7 +634,7 @@ describe('Compound ID tests', () => { where: { id1_id2: { id1: 1, id2: 1 } }, create: { id1: 1, id2: 1, title: 'Post1' }, update: { title: 'Post1-1' }, - }) + }), ).resolves.toMatchObject({ title: 'Post1-1' }); }); @@ -663,14 +649,14 @@ describe('Compound ID tests', () => { await expect( client.post.delete({ where: { id1_id2: { id1: 1, id2: 1 } }, - }) + }), ).resolves.toMatchObject({ title: 'Post1' }); // toplevel await expect( client.post.delete({ where: { id1_id2: { id1: 1, id2: 1 } }, - }) + }), ).toBeRejectedNotFound(); }); }); diff --git a/packages/runtime/test/client-api/count.test.ts b/packages/runtime/test/client-api/count.test.ts index e2b580e9..86e22264 100644 --- a/packages/runtime/test/client-api/count.test.ts +++ b/packages/runtime/test/client-api/count.test.ts @@ -5,90 +5,87 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-count-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client count tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client count tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); - }); + beforeEach(async () => { + client = await createClient(); + }); - afterEach(async () => { - await client?.$disconnect(); - }); + afterEach(async () => { + await client?.$disconnect(); + }); - it('works with simple count', async () => { - await client.user.create({ - data: { - id: '1', - email: 'u1@test.com', - }, - }); + it('works with simple count', async () => { + await client.user.create({ + data: { + id: '1', + email: 'u1@test.com', + }, + }); - await client.user.create({ - data: { - id: '2', - email: 'u2@test.com', - }, - }); + await client.user.create({ + data: { + id: '2', + email: 'u2@test.com', + }, + }); - // without filter - let r = await client.user.count(); - expect(r).toBe(2); - expect(r).toBeTypeOf('number'); + // without filter + let r = await client.user.count(); + expect(r).toBe(2); + expect(r).toBeTypeOf('number'); - r = await client.user.count({ select: true }); - expect(r).toBe(2); - expect(r).toBeTypeOf('number'); + r = await client.user.count({ select: true }); + expect(r).toBe(2); + expect(r).toBeTypeOf('number'); - // with filter - await expect( - client.user.count({ - where: { - email: { - contains: 'u1', - }, + // with filter + await expect( + client.user.count({ + where: { + email: { + contains: 'u1', }, - }) - ).resolves.toBe(1); + }, + }), + ).resolves.toBe(1); - // with skip and take - await expect( - client.user.count({ - skip: 1, - take: 1, - }) - ).resolves.toBe(1); - await expect( - client.user.count({ - skip: 10, - }) - ).resolves.toBe(0); - }); + // with skip and take + await expect( + client.user.count({ + skip: 1, + take: 1, + }), + ).resolves.toBe(1); + await expect( + client.user.count({ + skip: 10, + }), + ).resolves.toBe(0); + }); - it('works with field count', async () => { - await client.user.create({ - data: { - id: '1', - email: 'u1@test.com', - name: 'User1', - }, - }); + it('works with field count', async () => { + await client.user.create({ + data: { + id: '1', + email: 'u1@test.com', + name: 'User1', + }, + }); - await client.user.create({ - data: { - id: '2', - email: 'u2@test.com', - name: null, - }, - }); + await client.user.create({ + data: { + id: '2', + email: 'u2@test.com', + name: null, + }, + }); - const r = await client.user.count({ - select: { _all: true, name: true }, - }); - expect(r._all).toBe(2); - expect(r.name).toBe(1); + const r = await client.user.count({ + select: { _all: true, name: true }, }); - } -); + expect(r._all).toBe(2); + expect(r.name).toBe(1); + }); +}); diff --git a/packages/runtime/test/client-api/create-many-and-return.test.ts b/packages/runtime/test/client-api/create-many-and-return.test.ts index c664fbdc..258194b2 100644 --- a/packages/runtime/test/client-api/create-many-and-return.test.ts +++ b/packages/runtime/test/client-api/create-many-and-return.test.ts @@ -5,91 +5,76 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-create-many-and-return-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client createManyAndReturn tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client createManyAndReturn tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); - }); - - afterEach(async () => { - await client?.$disconnect(); - }); + beforeEach(async () => { + client = await createClient(); + }); - it('works with toplevel createManyAndReturn', async () => { - // empty - await expect(client.user.createManyAndReturn()).toResolveWithLength( - 0 - ); + afterEach(async () => { + await client?.$disconnect(); + }); - // single - await expect( - client.user.createManyAndReturn({ - data: { - email: 'u1@test.com', - name: 'name', - }, - }) - ).resolves.toEqual([ - expect.objectContaining({ email: 'u1@test.com', name: 'name' }), - ]); + it('works with toplevel createManyAndReturn', async () => { + // empty + await expect(client.user.createManyAndReturn()).toResolveWithLength(0); - // multiple - let r = await client.user.createManyAndReturn({ - data: [{ email: 'u2@test.com' }, { email: 'u3@test.com' }], - }); - expect(r).toHaveLength(2); - expect(r).toEqual( - expect.arrayContaining([ - expect.objectContaining({ email: 'u2@test.com' }), - expect.objectContaining({ email: 'u3@test.com' }), - ]) - ); + // single + await expect( + client.user.createManyAndReturn({ + data: { + email: 'u1@test.com', + name: 'name', + }, + }), + ).resolves.toEqual([expect.objectContaining({ email: 'u1@test.com', name: 'name' })]); - // conflict - await expect( - client.user.createManyAndReturn({ - data: [{ email: 'u3@test.com' }, { email: 'u4@test.com' }], - }) - ).rejects.toThrow(); - await expect( - client.user.findUnique({ where: { email: 'u4@test.com' } }) - ).toResolveNull(); + // multiple + let r = await client.user.createManyAndReturn({ + data: [{ email: 'u2@test.com' }, { email: 'u3@test.com' }], + }); + expect(r).toHaveLength(2); + expect(r).toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'u2@test.com' }), + expect.objectContaining({ email: 'u3@test.com' }), + ]), + ); - // skip duplicates - r = await client.user.createManyAndReturn({ + // conflict + await expect( + client.user.createManyAndReturn({ data: [{ email: 'u3@test.com' }, { email: 'u4@test.com' }], - skipDuplicates: true, - }); - expect(r).toHaveLength(1); - expect(r).toEqual( - expect.arrayContaining([ - expect.objectContaining({ email: 'u4@test.com' }), - ]) - ); - await expect( - client.user.findUnique({ where: { email: 'u4@test.com' } }) - ).toResolveTruthy(); + }), + ).rejects.toThrow(); + await expect(client.user.findUnique({ where: { email: 'u4@test.com' } })).toResolveNull(); + + // skip duplicates + r = await client.user.createManyAndReturn({ + data: [{ email: 'u3@test.com' }, { email: 'u4@test.com' }], + skipDuplicates: true, }); + expect(r).toHaveLength(1); + expect(r).toEqual(expect.arrayContaining([expect.objectContaining({ email: 'u4@test.com' })])); + await expect(client.user.findUnique({ where: { email: 'u4@test.com' } })).toResolveTruthy(); + }); - it('works with select and omit', async () => { - let r = await client.user.createManyAndReturn({ - data: [{ email: 'u1@test.com', name: 'name' }], - select: { email: true }, - }); - expect(r[0]!.email).toBe('u1@test.com'); - // @ts-expect-error - expect(r[0]!.name).toBeUndefined(); + it('works with select and omit', async () => { + let r = await client.user.createManyAndReturn({ + data: [{ email: 'u1@test.com', name: 'name' }], + select: { email: true }, + }); + expect(r[0]!.email).toBe('u1@test.com'); + // @ts-expect-error + expect(r[0]!.name).toBeUndefined(); - r = await client.user.createManyAndReturn({ - data: [{ email: 'u2@test.com', name: 'name' }], - omit: { name: true }, - }); - expect(r[0]!.email).toBe('u2@test.com'); - // @ts-expect-error - expect(r[0]!.name).toBeUndefined(); + r = await client.user.createManyAndReturn({ + data: [{ email: 'u2@test.com', name: 'name' }], + omit: { name: true }, }); - } -); + expect(r[0]!.email).toBe('u2@test.com'); + // @ts-expect-error + expect(r[0]!.name).toBeUndefined(); + }); +}); diff --git a/packages/runtime/test/client-api/create-many.test.ts b/packages/runtime/test/client-api/create-many.test.ts index 5af5b4da..1c2e119e 100644 --- a/packages/runtime/test/client-api/create-many.test.ts +++ b/packages/runtime/test/client-api/create-many.test.ts @@ -5,64 +5,57 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-create-many-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client createMany tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client createMany tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); - }); - - afterEach(async () => { - await client?.$disconnect(); - }); - - it('works with toplevel createMany', async () => { - // empty - await expect(client.user.createMany()).resolves.toMatchObject({ - count: 0, - }); + beforeEach(async () => { + client = await createClient(); + }); - // single - await expect( - client.user.createMany({ - data: { - email: 'u1@test.com', - name: 'name', - }, - }) - ).resolves.toMatchObject({ - count: 1, - }); + afterEach(async () => { + await client?.$disconnect(); + }); - // multiple - await expect( - client.user.createMany({ - data: [{ email: 'u2@test.com' }, { email: 'u3@test.com' }], - }) - ).resolves.toMatchObject({ count: 2 }); - - // conflict - await expect( - client.user.createMany({ - data: [{ email: 'u3@test.com' }, { email: 'u4@test.com' }], - }) - ).rejects.toThrow(); - await expect( - client.user.findUnique({ where: { email: 'u4@test.com' } }) - ).toResolveNull(); + it('works with toplevel createMany', async () => { + // empty + await expect(client.user.createMany()).resolves.toMatchObject({ + count: 0, + }); - // skip duplicates - await expect( - client.user.createMany({ - data: [{ email: 'u3@test.com' }, { email: 'u4@test.com' }], - skipDuplicates: true, - }) - ).resolves.toMatchObject({ count: 1 }); - await expect( - client.user.findUnique({ where: { email: 'u4@test.com' } }) - ).toResolveTruthy(); + // single + await expect( + client.user.createMany({ + data: { + email: 'u1@test.com', + name: 'name', + }, + }), + ).resolves.toMatchObject({ + count: 1, }); - } -); + + // multiple + await expect( + client.user.createMany({ + data: [{ email: 'u2@test.com' }, { email: 'u3@test.com' }], + }), + ).resolves.toMatchObject({ count: 2 }); + + // conflict + await expect( + client.user.createMany({ + data: [{ email: 'u3@test.com' }, { email: 'u4@test.com' }], + }), + ).rejects.toThrow(); + await expect(client.user.findUnique({ where: { email: 'u4@test.com' } })).toResolveNull(); + + // skip duplicates + await expect( + client.user.createMany({ + data: [{ email: 'u3@test.com' }, { email: 'u4@test.com' }], + skipDuplicates: true, + }), + ).resolves.toMatchObject({ count: 1 }); + await expect(client.user.findUnique({ where: { email: 'u4@test.com' } })).toResolveTruthy(); + }); +}); diff --git a/packages/runtime/test/client-api/create.test.ts b/packages/runtime/test/client-api/create.test.ts index 95074c32..4004d7cc 100644 --- a/packages/runtime/test/client-api/create.test.ts +++ b/packages/runtime/test/client-api/create.test.ts @@ -6,312 +6,304 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-create-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client create tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client create tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); - }); + beforeEach(async () => { + client = await createClient(); + }); - afterEach(async () => { - await client?.$disconnect(); - }); + afterEach(async () => { + await client?.$disconnect(); + }); - it('works with toplevel create single', async () => { - const user = await client.user.create({ - data: { - email: 'u1@test.com', - name: 'name', - }, - }); - expect(user).toMatchObject({ - id: expect.any(String), + it('works with toplevel create single', async () => { + const user = await client.user.create({ + data: { email: 'u1@test.com', name: 'name', - }); + }, + }); + expect(user).toMatchObject({ + id: expect.any(String), + email: 'u1@test.com', + name: 'name', + }); - const user2 = await client.user.create({ - data: { - email: 'u2@test.com', - name: 'name', - }, - omit: { name: true }, - }); - expect(user2.email).toBe('u2@test.com'); - expect((user2 as any).name).toBeUndefined(); - // @ts-expect-error - console.log(user2.name); + const user2 = await client.user.create({ + data: { + email: 'u2@test.com', + name: 'name', + }, + omit: { name: true }, + }); + expect(user2.email).toBe('u2@test.com'); + expect((user2 as any).name).toBeUndefined(); + // @ts-expect-error + console.log(user2.name); - const user3 = await client.user.create({ - data: { - email: 'u3@test.com', - name: 'name', - posts: { create: { title: 'Post1' } }, - }, - include: { posts: true }, - omit: { name: true }, - }); - expect(user3.email).toBe('u3@test.com'); - expect(user3.posts).toHaveLength(1); - expect((user3 as any).name).toBeUndefined(); - // @ts-expect-error - console.log(user3.name); + const user3 = await client.user.create({ + data: { + email: 'u3@test.com', + name: 'name', + posts: { create: { title: 'Post1' } }, + }, + include: { posts: true }, + omit: { name: true }, }); + expect(user3.email).toBe('u3@test.com'); + expect(user3.posts).toHaveLength(1); + expect((user3 as any).name).toBeUndefined(); + // @ts-expect-error + console.log(user3.name); + }); - it('works with nested relation one-to-one, owner side', async () => { - const user = await client.user.create({ - data: { email: 'u1@test.com' }, - }); + it('works with nested relation one-to-one, owner side', async () => { + const user = await client.user.create({ + data: { email: 'u1@test.com' }, + }); - // Post owns the relation, user will be inline created - const post1 = await client.post.create({ - data: { - title: 'Post1', - author: { - create: { email: 'u2@test.com' }, - }, + // Post owns the relation, user will be inline created + const post1 = await client.post.create({ + data: { + title: 'Post1', + author: { + create: { email: 'u2@test.com' }, }, - include: { author: true }, - }); - expect(post1.authorId).toBeTruthy(); - expect(post1.author).toMatchObject({ email: 'u2@test.com' }); + }, + include: { author: true }, + }); + expect(post1.authorId).toBeTruthy(); + expect(post1.author).toMatchObject({ email: 'u2@test.com' }); - // create Post by connecting to existing User via FK - const post2 = await client.post.create({ - data: { title: 'Post2', authorId: user.id }, - }); - expect(post2).toMatchObject({ - id: expect.any(String), - title: 'Post2', - authorId: user.id, - }); + // create Post by connecting to existing User via FK + const post2 = await client.post.create({ + data: { title: 'Post2', authorId: user.id }, + }); + expect(post2).toMatchObject({ + id: expect.any(String), + title: 'Post2', + authorId: user.id, + }); - // create Post by connecting to existing User via relation - const post3 = await client.post.create({ - data: { - title: 'Post3', - author: { connect: { id: user.id } }, - }, - }); - expect(post3).toMatchObject({ - id: expect.any(String), + // create Post by connecting to existing User via relation + const post3 = await client.post.create({ + data: { title: 'Post3', - authorId: user.id, - }); + author: { connect: { id: user.id } }, + }, + }); + expect(post3).toMatchObject({ + id: expect.any(String), + title: 'Post3', + authorId: user.id, + }); - // connectOrCreate - connect - const post4 = await client.post.create({ - data: { - title: 'Post4', - author: { - connectOrCreate: { - where: { email: 'u1@test.com' }, - create: { email: 'u1@test.com' }, - }, + // connectOrCreate - connect + const post4 = await client.post.create({ + data: { + title: 'Post4', + author: { + connectOrCreate: { + where: { email: 'u1@test.com' }, + create: { email: 'u1@test.com' }, }, }, - }); - expect(post4).toMatchObject({ - authorId: user.id, - }); + }, + }); + expect(post4).toMatchObject({ + authorId: user.id, + }); - // connectOrCreate - create - const post5 = await client.post.create({ - data: { - title: 'Post5', - author: { - connectOrCreate: { - where: { email: 'u3@test.com' }, - create: { email: 'u3@test.com' }, - }, + // connectOrCreate - create + const post5 = await client.post.create({ + data: { + title: 'Post5', + author: { + connectOrCreate: { + where: { email: 'u3@test.com' }, + create: { email: 'u3@test.com' }, }, }, - include: { author: true }, - }); - expect(post5.author).toMatchObject({ email: 'u3@test.com' }); + }, + include: { author: true }, + }); + expect(post5.author).toMatchObject({ email: 'u3@test.com' }); - // validate relation connection - const u1Found = await client.user.findUniqueOrThrow({ - where: { id: user.id }, - include: { posts: true }, - }); - expect(u1Found.posts).toHaveLength(3); + // validate relation connection + const u1Found = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { posts: true }, }); + expect(u1Found.posts).toHaveLength(3); + }); - it('works with nested relation one-to-one, non-owner side', async () => { - const profile = await client.profile.create({ - data: { bio: 'My bio' }, - }); + it('works with nested relation one-to-one, non-owner side', async () => { + const profile = await client.profile.create({ + data: { bio: 'My bio' }, + }); - // User doesn't own the "profile" relation, profile will be created after user - const user1 = await client.user.create({ - data: { - email: 'u1@test.com', - profile: { create: { bio: 'My bio' } }, - }, - include: { profile: true }, - }); - expect(user1.profile?.bio).toBe('My bio'); + // User doesn't own the "profile" relation, profile will be created after user + const user1 = await client.user.create({ + data: { + email: 'u1@test.com', + profile: { create: { bio: 'My bio' } }, + }, + include: { profile: true }, + }); + expect(user1.profile?.bio).toBe('My bio'); - // connecting an existing profile - const user2 = await client.user.create({ - data: { - email: 'u2@test.com', - name: null, // explicit null - profile: { connect: { id: profile.id } }, - }, - include: { profile: true }, - }); - expect(user2.profile?.id).toBe(profile.id); + // connecting an existing profile + const user2 = await client.user.create({ + data: { + email: 'u2@test.com', + name: null, // explicit null + profile: { connect: { id: profile.id } }, + }, + include: { profile: true }, + }); + expect(user2.profile?.id).toBe(profile.id); - // connectOrCreate - connect - const user3 = await client.user.create({ - data: { - email: 'u3@test.com', - profile: { - connectOrCreate: { - where: { id: profile.id }, - create: { bio: 'My other bio' }, - }, + // connectOrCreate - connect + const user3 = await client.user.create({ + data: { + email: 'u3@test.com', + profile: { + connectOrCreate: { + where: { id: profile.id }, + create: { bio: 'My other bio' }, }, }, - include: { profile: true }, - }); - expect(user3.profile).toMatchObject({ - id: profile.id, - bio: 'My bio', - }); + }, + include: { profile: true }, + }); + expect(user3.profile).toMatchObject({ + id: profile.id, + bio: 'My bio', + }); - // connectOrCreate - create - const user4 = await client.user.create({ - data: { - email: 'u4@test.com', - profile: { - connectOrCreate: { - where: { id: 'non-existing-id' }, - create: { bio: 'My other bio' }, - }, + // connectOrCreate - create + const user4 = await client.user.create({ + data: { + email: 'u4@test.com', + profile: { + connectOrCreate: { + where: { id: 'non-existing-id' }, + create: { bio: 'My other bio' }, }, }, - include: { profile: true }, - }); - expect(user4.profile).toMatchObject({ - bio: 'My other bio', - }); + }, + include: { profile: true }, + }); + expect(user4.profile).toMatchObject({ + bio: 'My other bio', + }); - // validate relation connection - const profileFound = await client.profile.findUniqueOrThrow({ - where: { id: profile.id }, - }); - expect(profileFound.userId).toBe(user3.id); + // validate relation connection + const profileFound = await client.profile.findUniqueOrThrow({ + where: { id: profile.id }, }); + expect(profileFound.userId).toBe(user3.id); + }); - it('works with nested relation one-to-one multiple actions', async () => { - const u1 = await client.user.create({ - data: { email: 'u1@test.com' }, - }); + it('works with nested relation one-to-one multiple actions', async () => { + const u1 = await client.user.create({ + data: { email: 'u1@test.com' }, + }); - const post = await client.post.create({ - data: { - title: 'Post1', - author: { + const post = await client.post.create({ + data: { + title: 'Post1', + author: { + create: { email: 'u2@test.com' }, + connectOrCreate: { + where: { email: 'u1@test.com' }, create: { email: 'u2@test.com' }, - connectOrCreate: { - where: { email: 'u1@test.com' }, - create: { email: 'u2@test.com' }, - }, }, }, - }); - - expect(post.authorId).toBe(u1.id); - await expect(client.user.findMany()).resolves.toHaveLength(2); + }, }); - it('works with nested one to many single action', async () => { - // singular - const u1 = await client.user.create({ - data: { - email: 'u1@test.com', - name: 'name', - posts: { - create: { - title: 'Post1', - content: 'My post', - }, - }, - }, - include: { posts: true }, - }); - expect(u1.posts).toHaveLength(1); + expect(post.authorId).toBe(u1.id); + await expect(client.user.findMany()).resolves.toHaveLength(2); + }); - // plural - const u2 = await client.user.create({ - data: { - email: 'u2@test.com', - name: 'name', - posts: { - create: [ - { - title: 'Post2', - content: 'My post', - }, - { - title: 'Post3', - content: 'My post', - }, - ], + it('works with nested one to many single action', async () => { + // singular + const u1 = await client.user.create({ + data: { + email: 'u1@test.com', + name: 'name', + posts: { + create: { + title: 'Post1', + content: 'My post', }, }, - include: { posts: true }, - }); - expect(u2.posts).toEqual( - expect.arrayContaining([ - expect.objectContaining({ title: 'Post2' }), - expect.objectContaining({ title: 'Post3' }), - ]) - ); + }, + include: { posts: true }, + }); + expect(u1.posts).toHaveLength(1); - // mixed create and connect - const u3 = await client.user.create({ - data: { - email: 'u3@test.com', - posts: { - create: { - title: 'Post4', + // plural + const u2 = await client.user.create({ + data: { + email: 'u2@test.com', + name: 'name', + posts: { + create: [ + { + title: 'Post2', content: 'My post', }, - connect: [ - { id: u1.posts[0]!.id }, - { id: u2.posts[0]!.id }, - ], - }, + { + title: 'Post3', + content: 'My post', + }, + ], }, - include: { posts: true }, - }); - expect(u3.posts).toHaveLength(3); - expect(u3.posts.map((p) => p.title)).toEqual( - expect.arrayContaining(['Post1', 'Post2', 'Post4']) - ); + }, + include: { posts: true }, }); + expect(u2.posts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ title: 'Post2' }), + expect.objectContaining({ title: 'Post3' }), + ]), + ); - it('rejects empty relation payload', async () => { - await expect( - client.post.create({ - data: { title: 'Post1', author: {} }, - }) - ).rejects.toThrow('At least one action is required'); - - await expect( - client.user.create({ - data: { - email: 'u1@test.com', - posts: {}, + // mixed create and connect + const u3 = await client.user.create({ + data: { + email: 'u3@test.com', + posts: { + create: { + title: 'Post4', + content: 'My post', }, - }) - ).rejects.toThrow(QueryError); + connect: [{ id: u1.posts[0]!.id }, { id: u2.posts[0]!.id }], + }, + }, + include: { posts: true }, }); - } -); + expect(u3.posts).toHaveLength(3); + expect(u3.posts.map((p) => p.title)).toEqual(expect.arrayContaining(['Post1', 'Post2', 'Post4'])); + }); + + it('rejects empty relation payload', async () => { + await expect( + client.post.create({ + data: { title: 'Post1', author: {} }, + }), + ).rejects.toThrow('At least one action is required'); + + await expect( + client.user.create({ + data: { + email: 'u1@test.com', + posts: {}, + }, + }), + ).rejects.toThrow(QueryError); + }); +}); diff --git a/packages/runtime/test/client-api/default-values.test.ts b/packages/runtime/test/client-api/default-values.test.ts index f36b2c94..d5c93fbd 100644 --- a/packages/runtime/test/client-api/default-values.test.ts +++ b/packages/runtime/test/client-api/default-values.test.ts @@ -20,9 +20,7 @@ const schema = { }, uuid7: { type: 'String', - default: ExpressionUtils.call('uuid', [ - ExpressionUtils.literal(7), - ]), + default: ExpressionUtils.call('uuid', [ExpressionUtils.literal(7)]), }, cuid: { type: 'String', @@ -30,9 +28,7 @@ const schema = { }, cuid2: { type: 'String', - default: ExpressionUtils.call('cuid', [ - ExpressionUtils.literal(2), - ]), + default: ExpressionUtils.call('cuid', [ExpressionUtils.literal(2)]), }, nanoid: { type: 'String', @@ -40,9 +36,7 @@ const schema = { }, nanoid8: { type: 'String', - default: ExpressionUtils.call('nanoid', [ - ExpressionUtils.literal(8), - ]), + default: ExpressionUtils.call('nanoid', [ExpressionUtils.literal(8)]), }, ulid: { type: 'String', diff --git a/packages/runtime/test/client-api/delete-many.test.ts b/packages/runtime/test/client-api/delete-many.test.ts index e35b1bbb..86bc8a59 100644 --- a/packages/runtime/test/client-api/delete-many.test.ts +++ b/packages/runtime/test/client-api/delete-many.test.ts @@ -5,78 +5,75 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-delete-many-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client deleteMany tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client deleteMany tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); - }); + beforeEach(async () => { + client = await createClient(); + }); - afterEach(async () => { - await client?.$disconnect(); - }); + afterEach(async () => { + await client?.$disconnect(); + }); - it('works with toplevel deleteMany', async () => { - await client.user.create({ - data: { - id: '1', - email: 'u1@test.com', - }, - }); - await client.user.create({ - data: { - id: '2', - email: 'u2@test.com', - }, - }); + it('works with toplevel deleteMany', async () => { + await client.user.create({ + data: { + id: '1', + email: 'u1@test.com', + }, + }); + await client.user.create({ + data: { + id: '2', + email: 'u2@test.com', + }, + }); - // delete not found - await expect( - client.user.deleteMany({ - where: { email: 'u3@test.com' }, - }) - ).resolves.toMatchObject({ count: 0 }); - await expect(client.user.findMany()).toResolveWithLength(2); + // delete not found + await expect( + client.user.deleteMany({ + where: { email: 'u3@test.com' }, + }), + ).resolves.toMatchObject({ count: 0 }); + await expect(client.user.findMany()).toResolveWithLength(2); - // delete one - await expect( - client.user.deleteMany({ - where: { email: 'u1@test.com' }, - }) - ).resolves.toMatchObject({ count: 1 }); - await expect(client.user.findMany()).toResolveWithLength(1); + // delete one + await expect( + client.user.deleteMany({ + where: { email: 'u1@test.com' }, + }), + ).resolves.toMatchObject({ count: 1 }); + await expect(client.user.findMany()).toResolveWithLength(1); - // delete all - await expect(client.user.deleteMany()).resolves.toMatchObject({ - count: 1, - }); - await expect(client.user.findMany()).toResolveWithLength(0); + // delete all + await expect(client.user.deleteMany()).resolves.toMatchObject({ + count: 1, }); + await expect(client.user.findMany()).toResolveWithLength(0); + }); - it('works with deleteMany with limit', async () => { - await client.user.create({ - data: { id: '1', email: 'u1@test.com' }, - }); - await client.user.create({ - data: { id: '2', email: 'u2@test.com' }, - }); + it('works with deleteMany with limit', async () => { + await client.user.create({ + data: { id: '1', email: 'u1@test.com' }, + }); + await client.user.create({ + data: { id: '2', email: 'u2@test.com' }, + }); - await expect( - client.user.deleteMany({ - where: { email: 'u3@test.com' }, - limit: 1, - }) - ).resolves.toMatchObject({ count: 0 }); - await expect(client.user.findMany()).toResolveWithLength(2); + await expect( + client.user.deleteMany({ + where: { email: 'u3@test.com' }, + limit: 1, + }), + ).resolves.toMatchObject({ count: 0 }); + await expect(client.user.findMany()).toResolveWithLength(2); - await expect( - client.user.deleteMany({ - limit: 1, - }) - ).resolves.toMatchObject({ count: 1 }); - await expect(client.user.findMany()).toResolveWithLength(1); - }); - } -); + await expect( + client.user.deleteMany({ + limit: 1, + }), + ).resolves.toMatchObject({ count: 1 }); + await expect(client.user.findMany()).toResolveWithLength(1); + }); +}); diff --git a/packages/runtime/test/client-api/delete.test.ts b/packages/runtime/test/client-api/delete.test.ts index 17bb2b64..57d70c60 100644 --- a/packages/runtime/test/client-api/delete.test.ts +++ b/packages/runtime/test/client-api/delete.test.ts @@ -5,59 +5,56 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-delete-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client delete tests', - ({ createClient }) => { - let client: ClientContract; - - beforeEach(async () => { - client = await createClient(); - }); - - afterEach(async () => { - await client?.$disconnect(); +describe.each(createClientSpecs(PG_DB_NAME))('Client delete tests', ({ createClient }) => { + let client: ClientContract; + + beforeEach(async () => { + client = await createClient(); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); + + it('works with toplevel delete', async () => { + let user = await client.user.create({ + data: { + id: '1', + email: 'u1@test.com', + }, }); - it('works with toplevel delete', async () => { - let user = await client.user.create({ - data: { - id: '1', - email: 'u1@test.com', + // not found + await expect( + client.user.delete({ + where: { id: '2' }, + }), + ).toBeRejectedNotFound(); + + // found + await expect( + client.user.delete({ + where: { id: user.id }, + }), + ).resolves.toMatchObject(user); + + // include relations + user = await client.user.create({ + data: { + id: '1', + email: 'u1@test.com', + profile: { + create: { bio: 'Bio' }, }, - }); - - // not found - await expect( - client.user.delete({ - where: { id: '2' }, - }) - ).toBeRejectedNotFound(); - - // found - await expect( - client.user.delete({ - where: { id: user.id }, - }) - ).resolves.toMatchObject(user); - - // include relations - user = await client.user.create({ - data: { - id: '1', - email: 'u1@test.com', - profile: { - create: { bio: 'Bio' }, - }, - }, - }); - await expect( - client.user.delete({ - where: { id: user.id }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ bio: 'Bio' }), - }); + }, + }); + await expect( + client.user.delete({ + where: { id: user.id }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ bio: 'Bio' }), }); - } -); + }); +}); diff --git a/packages/runtime/test/client-api/filter.test.ts b/packages/runtime/test/client-api/filter.test.ts index 9c27350b..cda36583 100644 --- a/packages/runtime/test/client-api/filter.test.ts +++ b/packages/runtime/test/client-api/filter.test.ts @@ -5,557 +5,496 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-filter-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client filter tests for $provider', - ({ createClient }) => { - let client: ClientContract; - - beforeEach(async () => { - client = await createClient(); +describe.each(createClientSpecs(PG_DB_NAME))('Client filter tests for $provider', ({ createClient }) => { + let client: ClientContract; + + beforeEach(async () => { + client = await createClient(); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); + + async function createUser( + email = 'u1@test.com', + restFields: any = { + name: 'User1', + role: 'ADMIN', + profile: { create: { bio: 'My bio' } }, + }, + ) { + return client.user.create({ + data: { + ...restFields, + email, + }, }); + } - afterEach(async () => { - await client?.$disconnect(); - }); + async function createPosts(authorId: string) { + return [ + await client.post.create({ + data: { title: 'Post1', published: true, authorId }, + }), + await client.post.create({ + data: { title: 'Post2', published: false, authorId }, + }), + ] as const; + } - async function createUser( - email = 'u1@test.com', - restFields: any = { - name: 'User1', - role: 'ADMIN', - profile: { create: { bio: 'My bio' } }, - } - ) { - return client.user.create({ - data: { - ...restFields, - email, + it('supports string filters', async () => { + const user1 = await createUser('u1@test.com'); + const user2 = await createUser('u2@test.com', { name: null }); + + // equals + await expect(client.user.findFirst({ where: { id: user1.id } })).toResolveTruthy(); + await expect(client.user.findFirst({ where: { id: { equals: user1.id } } })).toResolveTruthy(); + await expect(client.user.findFirst({ where: { id: { equals: '1' } } })).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + id: user1.id, + name: null, }, - }); - } - - async function createPosts(authorId: string) { - return [ - await client.post.create({ - data: { title: 'Post1', published: true, authorId }, - }), - await client.post.create({ - data: { title: 'Post2', published: false, authorId }, - }), - ] as const; - } - - it('supports string filters', async () => { - const user1 = await createUser('u1@test.com'); - const user2 = await createUser('u2@test.com', { name: null }); - - // equals - await expect( - client.user.findFirst({ where: { id: user1.id } }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ where: { id: { equals: user1.id } } }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ where: { id: { equals: '1' } } }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { - id: user1.id, - name: null, - }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { - id: user1.id, - name: { equals: null }, - }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { - id: user2.id, - name: { equals: null }, - }, - }) - ).toResolveTruthy(); - - // case-insensitive - await expect( - client.user.findFirst({ - where: { email: { equals: 'u1@Test.com' } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { - email: { equals: 'u1@Test.com', mode: 'insensitive' }, - }, - }) - ).toResolveTruthy(); - - // in - await expect( - client.user.findFirst({ - where: { email: { in: [] } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { email: { in: ['u1@test.com', 'u3@test.com'] } }, - }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { email: { in: ['u3@test.com'] } }, - }) - ).toResolveFalsy(); - - // notIn - await expect( - client.user.findFirst({ - where: { email: { notIn: [] } }, - }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { email: { notIn: ['u1@test.com', 'u2@test.com'] } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { email: { notIn: ['u2@test.com'] } }, - }) - ).toResolveTruthy(); - - // lt/gt/lte/gte - await expect( - client.user.findMany({ - where: { email: { lt: 'a@test.com' } }, - }) - ).toResolveWithLength(0); - await expect( - client.user.findMany({ - where: { email: { lt: 'z@test.com' } }, - }) - ).toResolveWithLength(2); - await expect( - client.user.findMany({ - where: { email: { lte: 'u1@test.com' } }, - }) - ).toResolveWithLength(1); - await expect( - client.user.findMany({ - where: { email: { lte: 'u2@test.com' } }, - }) - ).toResolveWithLength(2); - await expect( - client.user.findMany({ - where: { email: { gt: 'a@test.com' } }, - }) - ).toResolveWithLength(2); - await expect( - client.user.findMany({ - where: { email: { gt: 'z@test.com' } }, - }) - ).toResolveWithLength(0); - await expect( - client.user.findMany({ - where: { email: { gte: 'u1@test.com' } }, - }) - ).toResolveWithLength(2); - await expect( - client.user.findMany({ - where: { email: { gte: 'u2@test.com' } }, - }) - ).toResolveWithLength(1); - - // contains - await expect( - client.user.findFirst({ - where: { email: { contains: '1@' } }, - }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { email: { contains: '3@' } }, - }) - ).toResolveFalsy(); - - // startsWith - await expect( - client.user.findFirst({ - where: { email: { startsWith: 'u1@' } }, - }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { email: { startsWith: '1@' } }, - }) - ).toResolveFalsy(); - - // endsWith - await expect( - client.user.findFirst({ - where: { email: { endsWith: '@test.com' } }, - }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { email: { endsWith: '@test' } }, - }) - ).toResolveFalsy(); - - // not - await expect( - client.user.findFirst({ - where: { email: { not: { contains: 'test' } } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { email: { not: { not: { contains: 'test' } } } }, - }) - ).toResolveTruthy(); + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + id: user1.id, + name: { equals: null }, + }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + id: user2.id, + name: { equals: null }, + }, + }), + ).toResolveTruthy(); + + // case-insensitive + await expect( + client.user.findFirst({ + where: { email: { equals: 'u1@Test.com' } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + email: { equals: 'u1@Test.com', mode: 'insensitive' }, + }, + }), + ).toResolveTruthy(); + + // in + await expect( + client.user.findFirst({ + where: { email: { in: [] } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { email: { in: ['u1@test.com', 'u3@test.com'] } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { email: { in: ['u3@test.com'] } }, + }), + ).toResolveFalsy(); + + // notIn + await expect( + client.user.findFirst({ + where: { email: { notIn: [] } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { email: { notIn: ['u1@test.com', 'u2@test.com'] } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { email: { notIn: ['u2@test.com'] } }, + }), + ).toResolveTruthy(); + + // lt/gt/lte/gte + await expect( + client.user.findMany({ + where: { email: { lt: 'a@test.com' } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { email: { lt: 'z@test.com' } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { email: { lte: 'u1@test.com' } }, + }), + ).toResolveWithLength(1); + await expect( + client.user.findMany({ + where: { email: { lte: 'u2@test.com' } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { email: { gt: 'a@test.com' } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { email: { gt: 'z@test.com' } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { email: { gte: 'u1@test.com' } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { email: { gte: 'u2@test.com' } }, + }), + ).toResolveWithLength(1); + + // contains + await expect( + client.user.findFirst({ + where: { email: { contains: '1@' } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { email: { contains: '3@' } }, + }), + ).toResolveFalsy(); + + // startsWith + await expect( + client.user.findFirst({ + where: { email: { startsWith: 'u1@' } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { email: { startsWith: '1@' } }, + }), + ).toResolveFalsy(); + + // endsWith + await expect( + client.user.findFirst({ + where: { email: { endsWith: '@test.com' } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { email: { endsWith: '@test' } }, + }), + ).toResolveFalsy(); + + // not + await expect( + client.user.findFirst({ + where: { email: { not: { contains: 'test' } } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { email: { not: { not: { contains: 'test' } } } }, + }), + ).toResolveTruthy(); + }); + + it('supports numeric filters', async () => { + await createUser('u1@test.com', { + profile: { create: { id: '1', age: 20, bio: 'My bio' } }, }); - - it('supports numeric filters', async () => { - await createUser('u1@test.com', { - profile: { create: { id: '1', age: 20, bio: 'My bio' } }, - }); - await createUser('u2@test.com', { - profile: { create: { id: '2', bio: 'My bio' } }, - }); - - // equals - await expect( - client.profile.findFirst({ where: { age: 20 } }) - ).resolves.toMatchObject({ id: '1' }); - await expect( - client.profile.findFirst({ where: { age: { equals: 20 } } }) - ).resolves.toMatchObject({ id: '1' }); - await expect( - client.profile.findFirst({ where: { age: { equals: 10 } } }) - ).toResolveFalsy(); - await expect( - client.profile.findFirst({ where: { age: null } }) - ).resolves.toMatchObject({ id: '2' }); - await expect( - client.profile.findFirst({ where: { age: { equals: null } } }) - ).resolves.toMatchObject({ id: '2' }); - - // in - await expect( - client.profile.findFirst({ where: { age: { in: [] } } }) - ).toResolveFalsy(); - await expect( - client.profile.findFirst({ where: { age: { in: [20, 21] } } }) - ).resolves.toMatchObject({ id: '1' }); - await expect( - client.profile.findFirst({ where: { age: { in: [21] } } }) - ).toResolveFalsy(); - - // notIn - await expect( - client.profile.findFirst({ where: { age: { notIn: [] } } }) - ).toResolveTruthy(); - await expect( - client.profile.findFirst({ - where: { age: { notIn: [20, 21] } }, - }) - ).toResolveFalsy(); - await expect( - client.profile.findFirst({ where: { age: { notIn: [21] } } }) - ).toResolveTruthy(); - - // lt/gt/lte/gte - await expect( - client.profile.findMany({ where: { age: { lt: 20 } } }) - ).toResolveWithLength(0); - await expect( - client.profile.findMany({ where: { age: { lt: 21 } } }) - ).toResolveWithLength(1); - await expect( - client.profile.findMany({ where: { age: { lte: 20 } } }) - ).toResolveWithLength(1); - await expect( - client.profile.findMany({ where: { age: { lte: 19 } } }) - ).toResolveWithLength(0); - await expect( - client.profile.findMany({ where: { age: { gt: 20 } } }) - ).toResolveWithLength(0); - await expect( - client.profile.findMany({ where: { age: { gt: 19 } } }) - ).toResolveWithLength(1); - await expect( - client.profile.findMany({ where: { age: { gte: 20 } } }) - ).toResolveWithLength(1); - await expect( - client.profile.findMany({ where: { age: { gte: 21 } } }) - ).toResolveWithLength(0); - - // not - await expect( - client.profile.findFirst({ - where: { age: { not: { equals: 20 } } }, - }) - ).toResolveFalsy(); - await expect( - client.profile.findFirst({ - where: { age: { not: { not: { equals: 20 } } } }, - }) - ).toResolveTruthy(); - await expect( - client.profile.findFirst({ - where: { age: { not: { equals: null } } }, - }) - ).toResolveTruthy(); - await expect( - client.profile.findFirst({ - where: { age: { not: { not: { equals: null } } } }, - }) - ).toResolveTruthy(); + await createUser('u2@test.com', { + profile: { create: { id: '2', bio: 'My bio' } }, }); - it('supports boolean filters', async () => { - const user = await createUser('u1@test.com', { - profile: { create: { id: '1', age: 20, bio: 'My bio' } }, - }); - const [post1, post2] = await createPosts(user.id); - - // equals - await expect( - client.post.findFirst({ where: { published: true } }) - ).resolves.toMatchObject(post1); - await expect( - client.post.findFirst({ - where: { published: { equals: false } }, - }) - ).resolves.toMatchObject(post2); - - // not - await expect( - client.post.findFirst({ - where: { published: { not: { equals: true } } }, - }) - ).resolves.toMatchObject(post2); - await expect( - client.post.findFirst({ - where: { published: { not: { not: { equals: true } } } }, - }) - ).resolves.toMatchObject(post1); + // equals + await expect(client.profile.findFirst({ where: { age: 20 } })).resolves.toMatchObject({ id: '1' }); + await expect(client.profile.findFirst({ where: { age: { equals: 20 } } })).resolves.toMatchObject({ id: '1' }); + await expect(client.profile.findFirst({ where: { age: { equals: 10 } } })).toResolveFalsy(); + await expect(client.profile.findFirst({ where: { age: null } })).resolves.toMatchObject({ id: '2' }); + await expect(client.profile.findFirst({ where: { age: { equals: null } } })).resolves.toMatchObject({ + id: '2', }); - it('supports date filters', async () => { - const user1 = await createUser('u1@test.com', { - createdAt: new Date(), - }); - const user2 = await createUser('u2@test.com', { - createdAt: new Date(Date.now() + 1000), - }); - - // equals - await expect( - client.user.findFirst({ - where: { createdAt: user2.createdAt }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { createdAt: user2.createdAt.toISOString() }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { createdAt: { equals: user2.createdAt } }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { - createdAt: { equals: user2.createdAt.toISOString() }, - }, - }) - ).resolves.toMatchObject(user2); - - // in - await expect( - client.user.findFirst({ where: { createdAt: { in: [] } } }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { createdAt: { in: [user2.createdAt] } }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { - createdAt: { in: [user2.createdAt.toISOString()] }, - }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { createdAt: { in: [new Date()] } }, - }) - ).toResolveFalsy(); - - // notIn - await expect( - client.user.findFirst({ where: { createdAt: { notIn: [] } } }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { createdAt: { notIn: [user1.createdAt] } }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { - createdAt: { notIn: [user1.createdAt.toISOString()] }, - }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { - createdAt: { - notIn: [user1.createdAt, user2.createdAt], - }, - }, - }) - ).toResolveFalsy(); - - // lt/gt/lte/gte - await expect( - client.user.findFirst({ - where: { createdAt: { lt: user1.createdAt } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { createdAt: { lt: user2.createdAt } }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - where: { createdAt: { lte: user1.createdAt } }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findMany({ - where: { createdAt: { lte: user2.createdAt } }, - }) - ).toResolveWithLength(2); - await expect( - client.user.findFirst({ - where: { createdAt: { gt: user2.createdAt } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { createdAt: { gt: user1.createdAt } }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findMany({ - where: { createdAt: { gte: user1.createdAt } }, - }) - ).toResolveWithLength(2); - await expect( - client.user.findFirst({ - where: { createdAt: { gte: user2.createdAt } }, - }) - ).resolves.toMatchObject(user2); - - // not - await expect( - client.user.findFirst({ - where: { createdAt: { not: { equals: user1.createdAt } } }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { - createdAt: { - not: { not: { equals: user1.createdAt } }, - }, - }, - }) - ).resolves.toMatchObject(user1); + // in + await expect(client.profile.findFirst({ where: { age: { in: [] } } })).toResolveFalsy(); + await expect(client.profile.findFirst({ where: { age: { in: [20, 21] } } })).resolves.toMatchObject({ + id: '1', }); - - it('supports enum filters', async () => { - await createUser(); - - // equals - await expect( - client.user.findFirst({ where: { role: 'ADMIN' } }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ where: { role: 'USER' } }) - ).toResolveFalsy(); - - // in - await expect( - client.user.findFirst({ where: { role: { in: [] } } }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ where: { role: { in: ['ADMIN'] } } }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ where: { role: { in: ['USER'] } } }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { role: { in: ['ADMIN', 'USER'] } }, - }) - ).toResolveTruthy(); - - // notIn - await expect( - client.user.findFirst({ where: { role: { notIn: [] } } }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { role: { notIn: ['ADMIN'] } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { role: { notIn: ['USER'] } }, - }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { role: { notIn: ['ADMIN', 'USER'] } }, - }) - ).toResolveFalsy(); - - // not - await expect( - client.user.findFirst({ - where: { role: { not: { equals: 'ADMIN' } } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { role: { not: { not: { equals: 'ADMIN' } } } }, - }) - ).toResolveTruthy(); + await expect(client.profile.findFirst({ where: { age: { in: [21] } } })).toResolveFalsy(); + + // notIn + await expect(client.profile.findFirst({ where: { age: { notIn: [] } } })).toResolveTruthy(); + await expect( + client.profile.findFirst({ + where: { age: { notIn: [20, 21] } }, + }), + ).toResolveFalsy(); + await expect(client.profile.findFirst({ where: { age: { notIn: [21] } } })).toResolveTruthy(); + + // lt/gt/lte/gte + await expect(client.profile.findMany({ where: { age: { lt: 20 } } })).toResolveWithLength(0); + await expect(client.profile.findMany({ where: { age: { lt: 21 } } })).toResolveWithLength(1); + await expect(client.profile.findMany({ where: { age: { lte: 20 } } })).toResolveWithLength(1); + await expect(client.profile.findMany({ where: { age: { lte: 19 } } })).toResolveWithLength(0); + await expect(client.profile.findMany({ where: { age: { gt: 20 } } })).toResolveWithLength(0); + await expect(client.profile.findMany({ where: { age: { gt: 19 } } })).toResolveWithLength(1); + await expect(client.profile.findMany({ where: { age: { gte: 20 } } })).toResolveWithLength(1); + await expect(client.profile.findMany({ where: { age: { gte: 21 } } })).toResolveWithLength(0); + + // not + await expect( + client.profile.findFirst({ + where: { age: { not: { equals: 20 } } }, + }), + ).toResolveFalsy(); + await expect( + client.profile.findFirst({ + where: { age: { not: { not: { equals: 20 } } } }, + }), + ).toResolveTruthy(); + await expect( + client.profile.findFirst({ + where: { age: { not: { equals: null } } }, + }), + ).toResolveTruthy(); + await expect( + client.profile.findFirst({ + where: { age: { not: { not: { equals: null } } } }, + }), + ).toResolveTruthy(); + }); + + it('supports boolean filters', async () => { + const user = await createUser('u1@test.com', { + profile: { create: { id: '1', age: 20, bio: 'My bio' } }, }); - - it('ignores undefined filters', async () => { - await createUser(); - await expect( - client.user.findMany({ where: { id: undefined } }) - ).toResolveWithLength(1); + const [post1, post2] = await createPosts(user.id); + + // equals + await expect(client.post.findFirst({ where: { published: true } })).resolves.toMatchObject(post1); + await expect( + client.post.findFirst({ + where: { published: { equals: false } }, + }), + ).resolves.toMatchObject(post2); + + // not + await expect( + client.post.findFirst({ + where: { published: { not: { equals: true } } }, + }), + ).resolves.toMatchObject(post2); + await expect( + client.post.findFirst({ + where: { published: { not: { not: { equals: true } } } }, + }), + ).resolves.toMatchObject(post1); + }); + + it('supports date filters', async () => { + const user1 = await createUser('u1@test.com', { + createdAt: new Date(), + }); + const user2 = await createUser('u2@test.com', { + createdAt: new Date(Date.now() + 1000), }); - // TODO: filter for bigint, decimal, bytes - } -); + // equals + await expect( + client.user.findFirst({ + where: { createdAt: user2.createdAt }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { createdAt: user2.createdAt.toISOString() }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { createdAt: { equals: user2.createdAt } }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { + createdAt: { equals: user2.createdAt.toISOString() }, + }, + }), + ).resolves.toMatchObject(user2); + + // in + await expect(client.user.findFirst({ where: { createdAt: { in: [] } } })).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { createdAt: { in: [user2.createdAt] } }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { + createdAt: { in: [user2.createdAt.toISOString()] }, + }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { createdAt: { in: [new Date()] } }, + }), + ).toResolveFalsy(); + + // notIn + await expect(client.user.findFirst({ where: { createdAt: { notIn: [] } } })).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { createdAt: { notIn: [user1.createdAt] } }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { + createdAt: { notIn: [user1.createdAt.toISOString()] }, + }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { + createdAt: { + notIn: [user1.createdAt, user2.createdAt], + }, + }, + }), + ).toResolveFalsy(); + + // lt/gt/lte/gte + await expect( + client.user.findFirst({ + where: { createdAt: { lt: user1.createdAt } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { createdAt: { lt: user2.createdAt } }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + where: { createdAt: { lte: user1.createdAt } }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findMany({ + where: { createdAt: { lte: user2.createdAt } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findFirst({ + where: { createdAt: { gt: user2.createdAt } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { createdAt: { gt: user1.createdAt } }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findMany({ + where: { createdAt: { gte: user1.createdAt } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findFirst({ + where: { createdAt: { gte: user2.createdAt } }, + }), + ).resolves.toMatchObject(user2); + + // not + await expect( + client.user.findFirst({ + where: { createdAt: { not: { equals: user1.createdAt } } }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { + createdAt: { + not: { not: { equals: user1.createdAt } }, + }, + }, + }), + ).resolves.toMatchObject(user1); + }); + + it('supports enum filters', async () => { + await createUser(); + + // equals + await expect(client.user.findFirst({ where: { role: 'ADMIN' } })).toResolveTruthy(); + await expect(client.user.findFirst({ where: { role: 'USER' } })).toResolveFalsy(); + + // in + await expect(client.user.findFirst({ where: { role: { in: [] } } })).toResolveFalsy(); + await expect(client.user.findFirst({ where: { role: { in: ['ADMIN'] } } })).toResolveTruthy(); + await expect(client.user.findFirst({ where: { role: { in: ['USER'] } } })).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { role: { in: ['ADMIN', 'USER'] } }, + }), + ).toResolveTruthy(); + + // notIn + await expect(client.user.findFirst({ where: { role: { notIn: [] } } })).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { role: { notIn: ['ADMIN'] } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { role: { notIn: ['USER'] } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { role: { notIn: ['ADMIN', 'USER'] } }, + }), + ).toResolveFalsy(); + + // not + await expect( + client.user.findFirst({ + where: { role: { not: { equals: 'ADMIN' } } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { role: { not: { not: { equals: 'ADMIN' } } } }, + }), + ).toResolveTruthy(); + }); + + it('ignores undefined filters', async () => { + await createUser(); + await expect(client.user.findMany({ where: { id: undefined } })).toResolveWithLength(1); + }); + + // TODO: filter for bigint, decimal, bytes +}); diff --git a/packages/runtime/test/client-api/find.test.ts b/packages/runtime/test/client-api/find.test.ts index 80b59055..6d70c769 100644 --- a/packages/runtime/test/client-api/find.test.ts +++ b/packages/runtime/test/client-api/find.test.ts @@ -7,943 +7,896 @@ import { createPosts, createUser } from './utils'; const PG_DB_NAME = 'client-api-find-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client find tests for $provider', - ({ createClient }) => { - let client: ClientContract; - - beforeEach(async () => { - client = await createClient(); - }); - - afterEach(async () => { - await client?.$disconnect(); - }); - - it('returns correct data rows', async () => { - let r = await client.user.findMany(); - expect(r).toHaveLength(0); - - const user = await createUser(client, 'u1@test.com'); - await createPosts(client, user.id); - - r = await client.user.findMany(); - expect(r).toHaveLength(1); - expect(r[0]?.createdAt).toBeInstanceOf(Date); - - r = await client.user.findMany({ where: { id: user.id } }); - expect(r).toHaveLength(1); - - const post = await client.post.findFirst(); - expect(post?.published).toBeTypeOf('boolean'); - - r = await client.user.findMany({ where: { id: 'none' } }); - expect(r).toHaveLength(0); - - await createUser(client, 'u2@test.com'); - - await expect(client.user.findMany()).resolves.toHaveLength(2); - await expect( - client.user.findMany({ where: { email: 'u2@test.com' } }) - ).resolves.toHaveLength(1); - }); - - it('works with take and skip', async () => { - await createUser(client, 'u1@test.com', { id: '1' }); - await createUser(client, 'u02@test.com', { id: '2' }); - await createUser(client, 'u3@test.com', { id: '3' }); - - // take - await expect( - client.user.findMany({ take: 1 }) - ).resolves.toHaveLength(1); - // default sorted by id - await expect( - client.user.findFirst({ take: 1 }) - ).resolves.toMatchObject({ - email: 'u1@test.com', - }); - await expect( - client.user.findMany({ take: 2 }) - ).resolves.toHaveLength(2); - await expect( - client.user.findMany({ take: 4 }) - ).resolves.toHaveLength(3); - - // skip - await expect( - client.user.findMany({ skip: 1 }) - ).resolves.toHaveLength(2); - await expect( - client.user.findMany({ skip: 2 }) - ).resolves.toHaveLength(1); - // default sorted by id - await expect( - client.user.findFirst({ skip: 1, take: 1 }) - ).resolves.toMatchObject({ - email: 'u02@test.com', - }); - // explicit sort - await expect( - client.user.findFirst({ - skip: 2, - orderBy: { email: 'desc' }, - }) - ).resolves.toMatchObject({ - email: 'u02@test.com', - }); - - // take + skip - await expect( - client.user.findMany({ take: 1, skip: 1 }) - ).resolves.toHaveLength(1); - await expect( - client.user.findMany({ take: 3, skip: 2 }) - ).resolves.toHaveLength(1); - - // negative take, default sort is negated - await expect( - client.user.findMany({ take: -2 }) - ).toResolveWithLength(2); - await expect(client.user.findMany({ take: -2 })).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: '3' }), - expect.objectContaining({ id: '2' }), - ]) - ); - await expect( - client.user.findMany({ skip: 1, take: -1 }) - ).resolves.toEqual([expect.objectContaining({ id: '2' })]); - - // negative take, explicit sort is negated - await expect( - client.user.findMany({ - skip: 1, - take: -2, - orderBy: { email: 'asc' }, - }) - ).resolves.toEqual([ - expect.objectContaining({ email: 'u02@test.com' }), - expect.objectContaining({ email: 'u1@test.com' }), - ]); - }); - - it('works with orderBy', async () => { - const user1 = await createUser(client, 'u1@test.com', { - role: 'USER', - name: null, - profile: { create: { bio: 'My bio' } }, - }); - const user2 = await createUser(client, 'u2@test.com', { - role: 'ADMIN', - name: 'User2', - profile: { create: { bio: 'My other bio' } }, - }); - await createPosts(client, user1.id); - - await expect( - client.user.findFirst({ orderBy: { email: 'asc' } }) - ).resolves.toMatchObject({ email: 'u1@test.com' }); - - await expect( - client.user.findFirst({ orderBy: { email: 'desc' } }) - ).resolves.toMatchObject({ email: 'u2@test.com' }); - - // multiple sorting conditions in one object - await expect( - client.user.findFirst({ - orderBy: { role: 'asc', email: 'desc' }, - }) - ).resolves.toMatchObject({ email: 'u2@test.com' }); - - // multiple sorting conditions in array - await expect( - client.user.findFirst({ - orderBy: [{ role: 'asc' }, { email: 'desc' }], - }) - ).resolves.toMatchObject({ email: 'u2@test.com' }); - - // null first - await expect( - client.user.findFirst({ - orderBy: { name: { sort: 'asc', nulls: 'first' } }, - }) - ).resolves.toMatchObject({ email: 'u1@test.com' }); - - // null last - await expect( - client.user.findFirst({ - orderBy: { name: { sort: 'asc', nulls: 'last' } }, - }) - ).resolves.toMatchObject({ email: 'u2@test.com' }); - - // by to-many relation - await expect( - client.user.findFirst({ - orderBy: { posts: { _count: 'desc' } }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - orderBy: { posts: { _count: 'asc' } }, - }) - ).resolves.toMatchObject(user2); - - // by to-one relation - await expect( - client.user.findFirst({ - orderBy: { profile: { bio: 'asc' } }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - orderBy: { profile: { bio: 'desc' } }, - }) - ).resolves.toMatchObject(user2); - }); - - it('works with cursor', async () => { - const user1 = await createUser(client, 'u1@test.com', { - id: '1', - role: 'ADMIN', - }); - const user2 = await createUser(client, 'u2@test.com', { - id: '2', - role: 'USER', - }); - const user3 = await createUser(client, 'u3@test.com', { - id: '3', - role: 'ADMIN', - }); - - // cursor is inclusive - await expect( - client.user.findMany({ - cursor: { id: user2.id }, - }) - ).resolves.toEqual([user2, user3]); - - // skip cursor - await expect( - client.user.findMany({ - skip: 1, - cursor: { id: user1.id }, - }) - ).resolves.toEqual([user2, user3]); +describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', ({ createClient }) => { + let client: ClientContract; - // custom orderBy - await expect( - client.user.findMany({ - skip: 1, - cursor: { id: user2.id }, - orderBy: { email: 'desc' }, - }) - ).resolves.toEqual([user1]); - - // multiple orderBy - await expect( - client.user.findMany({ - skip: 1, - cursor: { id: user1.id }, - orderBy: [{ role: 'desc' }, { id: 'asc' }], - }) - ).resolves.toEqual([user3]); - - // multiple cursor - await expect( - client.user.findMany({ - skip: 1, - cursor: { id: user1.id, role: 'ADMIN' }, - }) - ).resolves.toEqual([user2, user3]); + beforeEach(async () => { + client = await createClient(); + }); - // non-existing cursor - await expect( - client.user.findMany({ - skip: 1, - cursor: { id: 'none' }, - }) - ).resolves.toEqual([]); + afterEach(async () => { + await client?.$disconnect(); + }); - // backward from cursor - await expect( - client.user.findMany({ - skip: 1, - take: -2, - cursor: { id: user3.id }, - }) - ).resolves.toEqual([user1, user2]); - }); - - it('works with distinct', async () => { - await createUser(client, 'u1@test.com', { - name: 'Admin1', - role: 'ADMIN', - }); - await createUser(client, 'u3@test.com', { - name: 'User', - role: 'USER', - }); - await createUser(client, 'u2@test.com', { - name: 'Admin2', - role: 'ADMIN', - }); - await createUser(client, 'u4@test.com', { - name: 'User', - role: 'USER', - }); - - // single field distinct - let r = await client.user.findMany({ distinct: ['role'] }); - expect(r).toHaveLength(2); - expect(r).toEqual( - expect.arrayContaining([ - expect.objectContaining({ role: 'ADMIN' }), - expect.objectContaining({ role: 'USER' }), - ]) - ); - - // multiple fields distinct - r = await client.user.findMany({ - distinct: ['role', 'name'], - }); - expect(r).toHaveLength(3); - expect(r).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'Admin1', role: 'ADMIN' }), - expect.objectContaining({ name: 'Admin2', role: 'ADMIN' }), - expect.objectContaining({ name: 'User', role: 'USER' }), - ]) - ); - }); - - it('works with nested skip, take, orderBy', async () => { - await createUser(client, 'u1@test.com', { - posts: { - create: [ - { id: '1', title: 'Post1' }, - { id: '2', title: 'Post2' }, - { id: '3', title: 'Post3' }, - ], - }, - }); - - await expect( - client.user.findFirst({ - include: { - posts: { orderBy: { title: 'desc' }, skip: 2, take: 1 }, - }, - }) - ).resolves.toEqual( - expect.objectContaining({ - posts: [expect.objectContaining({ id: '1' })], - }) - ); - - await expect( - client.user.findFirst({ - include: { - posts: { - skip: 1, - take: -2, - }, - }, - }) - ).resolves.toEqual( - expect.objectContaining({ - posts: [ - expect.objectContaining({ id: '1' }), - expect.objectContaining({ id: '2' }), - ], - }) - ); - }); - - it('works with unique finds', async () => { - let r = await client.user.findUnique({ where: { id: 'none' } }); - expect(r).toBeNull(); - - const user = await createUser(client); - - r = await client.user.findUnique({ where: { id: user.id } }); - expect(r).toMatchObject({ id: user.id, email: 'u1@test.com' }); - r = await client.user.findUnique({ - where: { email: 'u1@test.com' }, - }); - expect(r).toMatchObject({ id: user.id, email: 'u1@test.com' }); - - r = await client.user.findUnique({ where: { id: 'none' } }); - expect(r).toBeNull(); - await expect( - client.user.findUniqueOrThrow({ where: { id: 'none' } }) - ).rejects.toThrow(NotFoundError); - }); - - it('works with non-unique finds', async () => { - let r = await client.user.findFirst({ where: { name: 'User1' } }); - expect(r).toBeNull(); - - const user = await createUser(client); - - r = await client.user.findFirst({ where: { name: 'User1' } }); - expect(r).toMatchObject({ id: user.id, email: 'u1@test.com' }); - - r = await client.user.findFirst({ where: { name: 'User2' } }); - expect(r).toBeNull(); - await expect( - client.user.findFirstOrThrow({ where: { name: 'User2' } }) - ).rejects.toThrow(NotFoundError); - }); - - it('works with boolean composition', async () => { - const user1 = await createUser(client, 'u1@test.com'); - const user2 = await createUser(client, 'u2@test.com'); - - // AND - await expect( - client.user.findMany({ where: { AND: [] } }) - ).resolves.toHaveLength(2); - await expect( - client.user.findFirst({ - where: { - AND: { id: user1.id }, - }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - where: { - AND: [{ id: user1.id }], - }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - where: { - AND: [{ id: user1.id, email: 'u1@test.com' }], - }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - where: { - AND: [{ id: user1.id }, { email: 'u1@test.com' }], - }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - where: { - AND: [{ id: user1.id, email: 'u2@test.com' }], - }, - }) - ).toResolveFalsy(); - - // OR - await expect( - client.user.findMany({ where: { OR: [] } }) - ).resolves.toHaveLength(0); - await expect( - client.user.findFirst({ - where: { - OR: [{ id: user1.id }], - }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - where: { - OR: [{ id: user1.id, email: 'u2@test.com' }], - }, - }) - ).toResolveFalsy(); - await expect( - client.user.findMany({ - where: { - OR: [{ id: user1.id }, { email: 'u2@test.com' }], - }, - }) - ).resolves.toHaveLength(2); - await expect( - client.user.findFirst({ - where: { - OR: [{ id: 'foo', email: 'bar' }], - }, - }) - ).toResolveFalsy(); - - // NOT - await expect( - client.user.findMany({ where: { NOT: [] } }) - ).resolves.toHaveLength(0); - await expect( - client.user.findFirst({ - where: { - NOT: { id: user1.id }, - }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { - NOT: [{ id: user1.id }], - }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { - NOT: [{ id: user1.id, email: 'u1@test.com' }], - }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { - NOT: [{ id: user1.id }, { email: 'u1@test.com' }], - }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findMany({ - where: { - NOT: [{ id: user1.id }, { email: 'foo' }], - }, - }) - ).resolves.toHaveLength(2); + it('returns correct data rows', async () => { + let r = await client.user.findMany(); + expect(r).toHaveLength(0); - // unique filter - await expect( - client.user.findUnique({ - where: { - id: user1.id, - AND: [{ email: user1.email }], - }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findUnique({ - where: { - id: user1.id, - AND: [{ email: user2.email }], - }, - }) - ).toResolveFalsy(); - - // nesting - await expect( - client.user.findFirst({ - where: { - AND: { - id: user1.id, - OR: [{ email: 'foo' }, { email: 'bar' }], - }, - }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { - AND: { - id: user1.id, - NOT: { OR: [{ email: 'foo' }, { email: 'bar' }] }, - }, - }, - }) - ).resolves.toMatchObject(user1); - }); - - it('allows filtering by to-many relations', async () => { - const user = await createUser(client); - await createPosts(client, user.id); - - // some - await expect( - client.user.findFirst({ - where: { posts: { some: { title: 'Post1' } } }, - }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { posts: { some: { title: 'Post3' } } }, - }) - ).toResolveFalsy(); - - // every - await expect( - client.user.findFirst({ - where: { posts: { every: { authorId: user.id } } }, - }) - ).toResolveTruthy(); - await expect( - client.user.findFirst({ - where: { posts: { every: { published: true } } }, - }) - ).toResolveFalsy(); - - // none - await expect( - client.user.findFirst({ - where: { posts: { none: { title: 'Post1' } } }, - }) - ).toResolveFalsy(); - await expect( - client.user.findFirst({ - where: { posts: { none: { title: 'Post3' } } }, - }) - ).toResolveTruthy(); - }); - - it('allows filtering by to-one relations', async () => { - const user1 = await createUser(client, 'u1@test.com'); - await createPosts(client, user1.id); - const user2 = await createUser(client, 'u2@test.com', { - profile: null, - }); - - // null check from non-owner side - await expect( - client.user.findFirst({ - where: { profile: null }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { profile: { is: null } }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findFirst({ - where: { profile: { isNot: null } }, - }) - ).resolves.toMatchObject(user1); - - // null check from owner side - await expect( - client.profile.findFirst({ where: { user: null } }) - ).toResolveFalsy(); - await expect( - client.profile.findFirst({ where: { user: { is: null } } }) - ).toResolveFalsy(); - await expect( - client.profile.findFirst({ where: { user: { isNot: null } } }) - ).toResolveTruthy(); - - // field checks - await expect( - client.user.findFirst({ - where: { profile: { bio: 'My bio' } }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - where: { profile: { bio: 'My other bio' } }, - }) - ).toResolveFalsy(); - - // is/isNot - await expect( - client.user.findFirst({ - where: { profile: { is: { bio: 'My bio' } } }, - }) - ).resolves.toMatchObject(user1); - await expect( - client.user.findFirst({ - where: { profile: { isNot: { bio: 'My bio' } } }, - }) - ).resolves.toMatchObject(user2); - await expect( - client.user.findMany({ - where: { profile: { isNot: { bio: 'My other bio' } } }, - }) - ).resolves.toHaveLength(2); - }); - - it('allows field selection', async () => { - const user = await createUser(client); - await createPosts(client, user.id); - - let r = await client.user.findUnique({ - where: { id: user.id }, - select: { id: true, email: true, posts: true }, - }); - expect(r?.id).toBeTruthy(); - expect(r?.email).toBeTruthy(); - expect('name' in r!).toBeFalsy(); - expect(r?.posts).toHaveLength(2); - expect(r?.posts[0]?.createdAt).toBeInstanceOf(Date); - expect(r?.posts[0]?.published).toBeTypeOf('boolean'); - - await expect( - client.user.findUnique({ - where: { id: user.id }, - select: { id: true, email: true }, - include: { posts: true }, - } as any) - ).rejects.toThrow('cannot be used together'); - - const r1 = await client.user.findUnique({ - where: { id: user.id }, - include: { posts: { include: { author: true } } }, - }); - expect(r1!.posts[0]!.author).toMatchObject({ - id: user.id, - email: 'u1@test.com', - createdAt: expect.any(Date), - }); - }); - - it('allows field omission', async () => { - const user = await createUser(client); - await createPosts(client, user.id); - - const r = await client.user.findFirstOrThrow({ - omit: { name: true }, - }); - expect('name' in r).toBeFalsy(); - expect(r.email).toBeTruthy(); - - // @ts-expect-error omit and select cannot be used together - client.user.findFirstOrThrow({ - omit: { name: true }, - select: { email: true }, - }); - - const r1 = await client.user.findFirstOrThrow({ - include: { posts: { omit: { published: true } } }, - }); - expect('published' in r1.posts[0]!).toBeFalsy(); - }); - - it('allows including relation', async () => { - const user = await createUser(client); - const [post1, post2] = await createPosts(client, user.id); - - let r = await client.user.findUniqueOrThrow({ - where: { id: user.id }, - include: { posts: { where: { title: 'Post1' } } }, - }); - expect(r.posts).toHaveLength(1); - expect(r.posts[0]?.title).toBe('Post1'); + const user = await createUser(client, 'u1@test.com'); + await createPosts(client, user.id); - r = await client.user.findUniqueOrThrow({ - where: { id: user.id }, - include: { posts: { where: { published: true } } }, - }); - expect(r.posts).toHaveLength(1); + r = await client.user.findMany(); + expect(r).toHaveLength(1); + expect(r[0]?.createdAt).toBeInstanceOf(Date); - r = await client.user.findUniqueOrThrow({ - where: { id: user.id }, - include: { posts: { where: { title: 'Post3' } } }, - }); - expect(r.posts).toHaveLength(0); + r = await client.user.findMany({ where: { id: user.id } }); + expect(r).toHaveLength(1); - const r1 = await client.post.findFirstOrThrow({ - include: { - author: { - include: { posts: { where: { title: 'Post1' } } }, - }, - }, - }); - expect(r1.author.posts).toHaveLength(1); + const post = await client.post.findFirst(); + expect(post?.published).toBeTypeOf('boolean'); + + r = await client.user.findMany({ where: { id: 'none' } }); + expect(r).toHaveLength(0); + + await createUser(client, 'u2@test.com'); - let r2 = await client.user.findFirstOrThrow({ + await expect(client.user.findMany()).resolves.toHaveLength(2); + await expect(client.user.findMany({ where: { email: 'u2@test.com' } })).resolves.toHaveLength(1); + }); + + it('works with take and skip', async () => { + await createUser(client, 'u1@test.com', { id: '1' }); + await createUser(client, 'u02@test.com', { id: '2' }); + await createUser(client, 'u3@test.com', { id: '3' }); + + // take + await expect(client.user.findMany({ take: 1 })).resolves.toHaveLength(1); + // default sorted by id + await expect(client.user.findFirst({ take: 1 })).resolves.toMatchObject({ + email: 'u1@test.com', + }); + await expect(client.user.findMany({ take: 2 })).resolves.toHaveLength(2); + await expect(client.user.findMany({ take: 4 })).resolves.toHaveLength(3); + + // skip + await expect(client.user.findMany({ skip: 1 })).resolves.toHaveLength(2); + await expect(client.user.findMany({ skip: 2 })).resolves.toHaveLength(1); + // default sorted by id + await expect(client.user.findFirst({ skip: 1, take: 1 })).resolves.toMatchObject({ + email: 'u02@test.com', + }); + // explicit sort + await expect( + client.user.findFirst({ + skip: 2, + orderBy: { email: 'desc' }, + }), + ).resolves.toMatchObject({ + email: 'u02@test.com', + }); + + // take + skip + await expect(client.user.findMany({ take: 1, skip: 1 })).resolves.toHaveLength(1); + await expect(client.user.findMany({ take: 3, skip: 2 })).resolves.toHaveLength(1); + + // negative take, default sort is negated + await expect(client.user.findMany({ take: -2 })).toResolveWithLength(2); + await expect(client.user.findMany({ take: -2 })).resolves.toEqual( + expect.arrayContaining([expect.objectContaining({ id: '3' }), expect.objectContaining({ id: '2' })]), + ); + await expect(client.user.findMany({ skip: 1, take: -1 })).resolves.toEqual([ + expect.objectContaining({ id: '2' }), + ]); + + // negative take, explicit sort is negated + await expect( + client.user.findMany({ + skip: 1, + take: -2, + orderBy: { email: 'asc' }, + }), + ).resolves.toEqual([ + expect.objectContaining({ email: 'u02@test.com' }), + expect.objectContaining({ email: 'u1@test.com' }), + ]); + }); + + it('works with orderBy', async () => { + const user1 = await createUser(client, 'u1@test.com', { + role: 'USER', + name: null, + profile: { create: { bio: 'My bio' } }, + }); + const user2 = await createUser(client, 'u2@test.com', { + role: 'ADMIN', + name: 'User2', + profile: { create: { bio: 'My other bio' } }, + }); + await createPosts(client, user1.id); + + await expect(client.user.findFirst({ orderBy: { email: 'asc' } })).resolves.toMatchObject({ + email: 'u1@test.com', + }); + + await expect(client.user.findFirst({ orderBy: { email: 'desc' } })).resolves.toMatchObject({ + email: 'u2@test.com', + }); + + // multiple sorting conditions in one object + await expect( + client.user.findFirst({ + orderBy: { role: 'asc', email: 'desc' }, + }), + ).resolves.toMatchObject({ email: 'u2@test.com' }); + + // multiple sorting conditions in array + await expect( + client.user.findFirst({ + orderBy: [{ role: 'asc' }, { email: 'desc' }], + }), + ).resolves.toMatchObject({ email: 'u2@test.com' }); + + // null first + await expect( + client.user.findFirst({ + orderBy: { name: { sort: 'asc', nulls: 'first' } }, + }), + ).resolves.toMatchObject({ email: 'u1@test.com' }); + + // null last + await expect( + client.user.findFirst({ + orderBy: { name: { sort: 'asc', nulls: 'last' } }, + }), + ).resolves.toMatchObject({ email: 'u2@test.com' }); + + // by to-many relation + await expect( + client.user.findFirst({ + orderBy: { posts: { _count: 'desc' } }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + orderBy: { posts: { _count: 'asc' } }, + }), + ).resolves.toMatchObject(user2); + + // by to-one relation + await expect( + client.user.findFirst({ + orderBy: { profile: { bio: 'asc' } }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + orderBy: { profile: { bio: 'desc' } }, + }), + ).resolves.toMatchObject(user2); + }); + + it('works with cursor', async () => { + const user1 = await createUser(client, 'u1@test.com', { + id: '1', + role: 'ADMIN', + }); + const user2 = await createUser(client, 'u2@test.com', { + id: '2', + role: 'USER', + }); + const user3 = await createUser(client, 'u3@test.com', { + id: '3', + role: 'ADMIN', + }); + + // cursor is inclusive + await expect( + client.user.findMany({ + cursor: { id: user2.id }, + }), + ).resolves.toEqual([user2, user3]); + + // skip cursor + await expect( + client.user.findMany({ + skip: 1, + cursor: { id: user1.id }, + }), + ).resolves.toEqual([user2, user3]); + + // custom orderBy + await expect( + client.user.findMany({ + skip: 1, + cursor: { id: user2.id }, + orderBy: { email: 'desc' }, + }), + ).resolves.toEqual([user1]); + + // multiple orderBy + await expect( + client.user.findMany({ + skip: 1, + cursor: { id: user1.id }, + orderBy: [{ role: 'desc' }, { id: 'asc' }], + }), + ).resolves.toEqual([user3]); + + // multiple cursor + await expect( + client.user.findMany({ + skip: 1, + cursor: { id: user1.id, role: 'ADMIN' }, + }), + ).resolves.toEqual([user2, user3]); + + // non-existing cursor + await expect( + client.user.findMany({ + skip: 1, + cursor: { id: 'none' }, + }), + ).resolves.toEqual([]); + + // backward from cursor + await expect( + client.user.findMany({ + skip: 1, + take: -2, + cursor: { id: user3.id }, + }), + ).resolves.toEqual([user1, user2]); + }); + + it('works with distinct', async () => { + await createUser(client, 'u1@test.com', { + name: 'Admin1', + role: 'ADMIN', + }); + await createUser(client, 'u3@test.com', { + name: 'User', + role: 'USER', + }); + await createUser(client, 'u2@test.com', { + name: 'Admin2', + role: 'ADMIN', + }); + await createUser(client, 'u4@test.com', { + name: 'User', + role: 'USER', + }); + + // single field distinct + let r = await client.user.findMany({ distinct: ['role'] }); + expect(r).toHaveLength(2); + expect(r).toEqual( + expect.arrayContaining([ + expect.objectContaining({ role: 'ADMIN' }), + expect.objectContaining({ role: 'USER' }), + ]), + ); + + // multiple fields distinct + r = await client.user.findMany({ + distinct: ['role', 'name'], + }); + expect(r).toHaveLength(3); + expect(r).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'Admin1', role: 'ADMIN' }), + expect.objectContaining({ name: 'Admin2', role: 'ADMIN' }), + expect.objectContaining({ name: 'User', role: 'USER' }), + ]), + ); + }); + + it('works with nested skip, take, orderBy', async () => { + await createUser(client, 'u1@test.com', { + posts: { + create: [ + { id: '1', title: 'Post1' }, + { id: '2', title: 'Post2' }, + { id: '3', title: 'Post3' }, + ], + }, + }); + + await expect( + client.user.findFirst({ include: { - profile: { where: { bio: 'My bio' } }, + posts: { orderBy: { title: 'desc' }, skip: 2, take: 1 }, }, - }); - expect(r2.profile).toBeTruthy(); - r2 = await client.user.findFirstOrThrow({ + }), + ).resolves.toEqual( + expect.objectContaining({ + posts: [expect.objectContaining({ id: '1' })], + }), + ); + + await expect( + client.user.findFirst({ include: { - profile: { where: { bio: 'Some bio' } }, + posts: { + skip: 1, + take: -2, + }, }, - }); - expect(r2.profile).toBeNull(); + }), + ).resolves.toEqual( + expect.objectContaining({ + posts: [expect.objectContaining({ id: '1' }), expect.objectContaining({ id: '2' })], + }), + ); + }); + + it('works with unique finds', async () => { + let r = await client.user.findUnique({ where: { id: 'none' } }); + expect(r).toBeNull(); + + const user = await createUser(client); + + r = await client.user.findUnique({ where: { id: user.id } }); + expect(r).toMatchObject({ id: user.id, email: 'u1@test.com' }); + r = await client.user.findUnique({ + where: { email: 'u1@test.com' }, + }); + expect(r).toMatchObject({ id: user.id, email: 'u1@test.com' }); - await expect( - client.post.findFirstOrThrow({ - // @ts-expect-error - include: { author: { where: { email: user.email } } }, - }) - ).rejects.toThrow(`Field "author" doesn't support filtering`); + r = await client.user.findUnique({ where: { id: 'none' } }); + expect(r).toBeNull(); + await expect(client.user.findUniqueOrThrow({ where: { id: 'none' } })).rejects.toThrow(NotFoundError); + }); - // sorting - let u = await client.user.findUniqueOrThrow({ - where: { id: user.id }, - include: { - posts: { - orderBy: { - published: 'asc', - }, + it('works with non-unique finds', async () => { + let r = await client.user.findFirst({ where: { name: 'User1' } }); + expect(r).toBeNull(); + + const user = await createUser(client); + + r = await client.user.findFirst({ where: { name: 'User1' } }); + expect(r).toMatchObject({ id: user.id, email: 'u1@test.com' }); + + r = await client.user.findFirst({ where: { name: 'User2' } }); + expect(r).toBeNull(); + await expect(client.user.findFirstOrThrow({ where: { name: 'User2' } })).rejects.toThrow(NotFoundError); + }); + + it('works with boolean composition', async () => { + const user1 = await createUser(client, 'u1@test.com'); + const user2 = await createUser(client, 'u2@test.com'); + + // AND + await expect(client.user.findMany({ where: { AND: [] } })).resolves.toHaveLength(2); + await expect( + client.user.findFirst({ + where: { + AND: { id: user1.id }, + }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + where: { + AND: [{ id: user1.id }], + }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + where: { + AND: [{ id: user1.id, email: 'u1@test.com' }], + }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + where: { + AND: [{ id: user1.id }, { email: 'u1@test.com' }], + }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + where: { + AND: [{ id: user1.id, email: 'u2@test.com' }], + }, + }), + ).toResolveFalsy(); + + // OR + await expect(client.user.findMany({ where: { OR: [] } })).resolves.toHaveLength(0); + await expect( + client.user.findFirst({ + where: { + OR: [{ id: user1.id }], + }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + where: { + OR: [{ id: user1.id, email: 'u2@test.com' }], + }, + }), + ).toResolveFalsy(); + await expect( + client.user.findMany({ + where: { + OR: [{ id: user1.id }, { email: 'u2@test.com' }], + }, + }), + ).resolves.toHaveLength(2); + await expect( + client.user.findFirst({ + where: { + OR: [{ id: 'foo', email: 'bar' }], + }, + }), + ).toResolveFalsy(); + + // NOT + await expect(client.user.findMany({ where: { NOT: [] } })).resolves.toHaveLength(0); + await expect( + client.user.findFirst({ + where: { + NOT: { id: user1.id }, + }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { + NOT: [{ id: user1.id }], + }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { + NOT: [{ id: user1.id, email: 'u1@test.com' }], + }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { + NOT: [{ id: user1.id }, { email: 'u1@test.com' }], + }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findMany({ + where: { + NOT: [{ id: user1.id }, { email: 'foo' }], + }, + }), + ).resolves.toHaveLength(2); + + // unique filter + await expect( + client.user.findUnique({ + where: { + id: user1.id, + AND: [{ email: user1.email }], + }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findUnique({ + where: { + id: user1.id, + AND: [{ email: user2.email }], + }, + }), + ).toResolveFalsy(); + + // nesting + await expect( + client.user.findFirst({ + where: { + AND: { + id: user1.id, + OR: [{ email: 'foo' }, { email: 'bar' }], }, }, - }); - expect(u.posts[0]).toMatchObject(post2); - u = await client.user.findUniqueOrThrow({ - where: { id: user.id }, - include: { - posts: { - orderBy: { - published: 'desc', - }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { + AND: { + id: user1.id, + NOT: { OR: [{ email: 'foo' }, { email: 'bar' }] }, }, }, - }); - expect(u.posts[0]).toMatchObject(post1); + }), + ).resolves.toMatchObject(user1); + }); + + it('allows filtering by to-many relations', async () => { + const user = await createUser(client); + await createPosts(client, user.id); + + // some + await expect( + client.user.findFirst({ + where: { posts: { some: { title: 'Post1' } } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { posts: { some: { title: 'Post3' } } }, + }), + ).toResolveFalsy(); + + // every + await expect( + client.user.findFirst({ + where: { posts: { every: { authorId: user.id } } }, + }), + ).toResolveTruthy(); + await expect( + client.user.findFirst({ + where: { posts: { every: { published: true } } }, + }), + ).toResolveFalsy(); + + // none + await expect( + client.user.findFirst({ + where: { posts: { none: { title: 'Post1' } } }, + }), + ).toResolveFalsy(); + await expect( + client.user.findFirst({ + where: { posts: { none: { title: 'Post3' } } }, + }), + ).toResolveTruthy(); + }); + + it('allows filtering by to-one relations', async () => { + const user1 = await createUser(client, 'u1@test.com'); + await createPosts(client, user1.id); + const user2 = await createUser(client, 'u2@test.com', { + profile: null, + }); - // skip and take - u = await client.user.findUniqueOrThrow({ + // null check from non-owner side + await expect( + client.user.findFirst({ + where: { profile: null }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { profile: { is: null } }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findFirst({ + where: { profile: { isNot: null } }, + }), + ).resolves.toMatchObject(user1); + + // null check from owner side + await expect(client.profile.findFirst({ where: { user: null } })).toResolveFalsy(); + await expect(client.profile.findFirst({ where: { user: { is: null } } })).toResolveFalsy(); + await expect(client.profile.findFirst({ where: { user: { isNot: null } } })).toResolveTruthy(); + + // field checks + await expect( + client.user.findFirst({ + where: { profile: { bio: 'My bio' } }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + where: { profile: { bio: 'My other bio' } }, + }), + ).toResolveFalsy(); + + // is/isNot + await expect( + client.user.findFirst({ + where: { profile: { is: { bio: 'My bio' } } }, + }), + ).resolves.toMatchObject(user1); + await expect( + client.user.findFirst({ + where: { profile: { isNot: { bio: 'My bio' } } }, + }), + ).resolves.toMatchObject(user2); + await expect( + client.user.findMany({ + where: { profile: { isNot: { bio: 'My other bio' } } }, + }), + ).resolves.toHaveLength(2); + }); + + it('allows field selection', async () => { + const user = await createUser(client); + await createPosts(client, user.id); + + const r = await client.user.findUnique({ + where: { id: user.id }, + select: { id: true, email: true, posts: true }, + }); + expect(r?.id).toBeTruthy(); + expect(r?.email).toBeTruthy(); + expect('name' in r!).toBeFalsy(); + expect(r?.posts).toHaveLength(2); + expect(r?.posts[0]?.createdAt).toBeInstanceOf(Date); + expect(r?.posts[0]?.published).toBeTypeOf('boolean'); + + await expect( + client.user.findUnique({ where: { id: user.id }, - include: { - posts: { - take: 1, - skip: 1, + select: { id: true, email: true }, + include: { posts: true }, + } as any), + ).rejects.toThrow('cannot be used together'); + + const r1 = await client.user.findUnique({ + where: { id: user.id }, + include: { posts: { include: { author: true } } }, + }); + expect(r1!.posts[0]!.author).toMatchObject({ + id: user.id, + email: 'u1@test.com', + createdAt: expect.any(Date), + }); + }); + + it('allows field omission', async () => { + const user = await createUser(client); + await createPosts(client, user.id); + + const r = await client.user.findFirstOrThrow({ + omit: { name: true }, + }); + expect('name' in r).toBeFalsy(); + expect(r.email).toBeTruthy(); + + // @ts-expect-error omit and select cannot be used together + client.user.findFirstOrThrow({ + omit: { name: true }, + select: { email: true }, + }); + + const r1 = await client.user.findFirstOrThrow({ + include: { posts: { omit: { published: true } } }, + }); + expect('published' in r1.posts[0]!).toBeFalsy(); + }); + + it('allows including relation', async () => { + const user = await createUser(client); + const [post1, post2] = await createPosts(client, user.id); + + let r = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { posts: { where: { title: 'Post1' } } }, + }); + expect(r.posts).toHaveLength(1); + expect(r.posts[0]?.title).toBe('Post1'); + + r = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { posts: { where: { published: true } } }, + }); + expect(r.posts).toHaveLength(1); + + r = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { posts: { where: { title: 'Post3' } } }, + }); + expect(r.posts).toHaveLength(0); + + const r1 = await client.post.findFirstOrThrow({ + include: { + author: { + include: { posts: { where: { title: 'Post1' } } }, + }, + }, + }); + expect(r1.author.posts).toHaveLength(1); + + let r2 = await client.user.findFirstOrThrow({ + include: { + profile: { where: { bio: 'My bio' } }, + }, + }); + expect(r2.profile).toBeTruthy(); + r2 = await client.user.findFirstOrThrow({ + include: { + profile: { where: { bio: 'Some bio' } }, + }, + }); + expect(r2.profile).toBeNull(); + + await expect( + client.post.findFirstOrThrow({ + // @ts-expect-error + include: { author: { where: { email: user.email } } }, + }), + ).rejects.toThrow(`Field "author" doesn't support filtering`); + + // sorting + let u = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { + posts: { + orderBy: { + published: 'asc', }, }, - }); - expect(u.posts).toHaveLength(1); - u = await client.user.findUniqueOrThrow({ - where: { id: user.id }, - include: { - posts: { - skip: 2, + }, + }); + expect(u.posts[0]).toMatchObject(post2); + u = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { + posts: { + orderBy: { + published: 'desc', }, }, - }); - expect(u.posts).toHaveLength(0); - }); - - it('support counting relations', async () => { - const user1 = await createUser(client, 'u1@test.com'); - const user2 = await createUser(client, 'u2@test.com'); - await createPosts(client, user1.id); - - await expect( - client.user.findUnique({ - where: { id: user1.id }, - select: { _count: true }, - }) - ).resolves.toMatchObject({ - _count: { posts: 2 }, - }); - - await expect( - client.user.findUnique({ - where: { id: user1.id }, - select: { _count: { select: { posts: true } } }, - }) - ).resolves.toMatchObject({ - _count: { posts: 2 }, - }); - - await expect( - client.user.findUnique({ - where: { id: user1.id }, - select: { - _count: { - select: { posts: { where: { title: 'Post1' } } }, - }, + }, + }); + expect(u.posts[0]).toMatchObject(post1); + + // skip and take + u = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { + posts: { + take: 1, + skip: 1, + }, + }, + }); + expect(u.posts).toHaveLength(1); + u = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { + posts: { + skip: 2, + }, + }, + }); + expect(u.posts).toHaveLength(0); + }); + + it('support counting relations', async () => { + const user1 = await createUser(client, 'u1@test.com'); + const user2 = await createUser(client, 'u2@test.com'); + await createPosts(client, user1.id); + + await expect( + client.user.findUnique({ + where: { id: user1.id }, + select: { _count: true }, + }), + ).resolves.toMatchObject({ + _count: { posts: 2 }, + }); + + await expect( + client.user.findUnique({ + where: { id: user1.id }, + select: { _count: { select: { posts: true } } }, + }), + ).resolves.toMatchObject({ + _count: { posts: 2 }, + }); + + await expect( + client.user.findUnique({ + where: { id: user1.id }, + select: { + _count: { + select: { posts: { where: { title: 'Post1' } } }, }, - }) - ).resolves.toMatchObject({ - _count: { posts: 1 }, - }); - - await expect( - client.user.findUnique({ - where: { id: user2.id }, - select: { _count: true }, - }) - ).resolves.toMatchObject({ - _count: { posts: 0 }, - }); - - await expect( - client.user.findUnique({ - where: { id: user1.id }, - select: { - _count: { - select: { - posts: true, - }, + }, + }), + ).resolves.toMatchObject({ + _count: { posts: 1 }, + }); + + await expect( + client.user.findUnique({ + where: { id: user2.id }, + select: { _count: true }, + }), + ).resolves.toMatchObject({ + _count: { posts: 0 }, + }); + + await expect( + client.user.findUnique({ + where: { id: user1.id }, + select: { + _count: { + select: { + posts: true, }, }, - }) - ).resolves.toMatchObject({ - _count: { posts: 2 }, - }); - - await expect( - client.user.findUnique({ - where: { id: user1.id }, - select: { - _count: { - select: { - posts: { where: { published: true } }, - }, + }, + }), + ).resolves.toMatchObject({ + _count: { posts: 2 }, + }); + + await expect( + client.user.findUnique({ + where: { id: user1.id }, + select: { + _count: { + select: { + posts: { where: { published: true } }, }, }, - }) - ).resolves.toMatchObject({ - _count: { posts: 1 }, - }); - - await expect( - client.user.findUnique({ - where: { id: user1.id }, - select: { - _count: { - select: { - posts: { - where: { author: { email: user1.email } }, - }, + }, + }), + ).resolves.toMatchObject({ + _count: { posts: 1 }, + }); + + await expect( + client.user.findUnique({ + where: { id: user1.id }, + select: { + _count: { + select: { + posts: { + where: { author: { email: user1.email } }, }, }, }, - }) - ).resolves.toMatchObject({ - _count: { posts: 2 }, - }); - - await expect( - client.user.findUnique({ - where: { id: user1.id }, - select: { - _count: { - select: { - posts: { - where: { author: { email: user2.email } }, - }, + }, + }), + ).resolves.toMatchObject({ + _count: { posts: 2 }, + }); + + await expect( + client.user.findUnique({ + where: { id: user1.id }, + select: { + _count: { + select: { + posts: { + where: { author: { email: user2.email } }, }, }, }, - }) - ).resolves.toMatchObject({ - _count: { posts: 0 }, - }); + }, + }), + ).resolves.toMatchObject({ + _count: { posts: 0 }, }); + }); - it('supports $expr', async () => { - await createUser(client, 'yiming@gmail.com'); - await createUser(client, 'yiming@zenstack.dev'); + it('supports $expr', async () => { + await createUser(client, 'yiming@gmail.com'); + await createUser(client, 'yiming@zenstack.dev'); - await expect( - client.user.findMany({ - where: { - role: 'ADMIN', - $expr: (eb) => eb('email', 'like', '%@zenstack.dev'), - }, - }) - ).resolves.toHaveLength(1); - - await expect( - client.user.findMany({ - where: { - role: 'USER', - $expr: (eb) => eb('email', 'like', '%@zenstack.dev'), - }, - }) - ).resolves.toHaveLength(0); - }); - } -); + await expect( + client.user.findMany({ + where: { + role: 'ADMIN', + $expr: (eb) => eb('email', 'like', '%@zenstack.dev'), + }, + }), + ).resolves.toHaveLength(1); + + await expect( + client.user.findMany({ + where: { + role: 'USER', + $expr: (eb) => eb('email', 'like', '%@zenstack.dev'), + }, + }), + ).resolves.toHaveLength(0); + }); +}); diff --git a/packages/runtime/test/client-api/group-by.test.ts b/packages/runtime/test/client-api/group-by.test.ts index 76cd6e5e..0e0256e2 100644 --- a/packages/runtime/test/client-api/group-by.test.ts +++ b/packages/runtime/test/client-api/group-by.test.ts @@ -6,191 +6,183 @@ import { createUser } from './utils'; const PG_DB_NAME = 'client-api-group-by-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client groupBy tests', - ({ createClient }) => { - let client: ClientContract; - - beforeEach(async () => { - client = await createClient(); +describe.each(createClientSpecs(PG_DB_NAME))('Client groupBy tests', ({ createClient }) => { + let client: ClientContract; + + beforeEach(async () => { + client = await createClient(); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); + + it('works with single by', async () => { + await createUser(client, 'u1@test.com', { + id: '1', + name: 'Admin', + role: 'ADMIN', }); - - afterEach(async () => { - await client?.$disconnect(); + await createUser(client, 'u2@test.com', { + id: '2', + name: 'User', + role: 'USER', }); + await createUser(client, 'u3@test.com', { + id: '3', + name: 'User', + role: 'USER', + }); + + await expect( + client.user.groupBy({ + by: ['name'], + _count: { + role: true, + }, + }), + ).resolves.toEqual( + expect.arrayContaining([ + { name: 'User', _count: { role: 2 } }, + { name: 'Admin', _count: { role: 1 } }, + ]), + ); + + await expect( + client.user.groupBy({ + by: ['email'], + where: { + email: { not: 'u2@test.com' }, + }, + skip: 1, + take: -1, + orderBy: { email: 'desc' }, + }), + ).resolves.toEqual([{ email: 'u1@test.com' }]); + + await expect( + client.user.groupBy({ + by: ['email'], + skip: 1, + take: -2, + orderBy: { email: 'desc' }, + }), + ).resolves.toEqual([{ email: 'u2@test.com' }, { email: 'u1@test.com' }]); + + await expect( + client.user.groupBy({ + by: ['name'], + _count: true, + having: { + name: 'User', + }, + }), + ).resolves.toEqual(expect.arrayContaining([{ name: 'User', _count: 2 }])); - it('works with single by', async () => { - await createUser(client, 'u1@test.com', { - id: '1', - name: 'Admin', - role: 'ADMIN', - }); - await createUser(client, 'u2@test.com', { - id: '2', - name: 'User', - role: 'USER', - }); - await createUser(client, 'u3@test.com', { - id: '3', - name: 'User', - role: 'USER', - }); - - await expect( - client.user.groupBy({ - by: ['name'], + await expect( + client.user.groupBy({ + by: ['name', 'role'], + orderBy: { _count: { - role: true, + role: 'desc', }, - }) - ).resolves.toEqual( - expect.arrayContaining([ - { name: 'User', _count: { role: 2 } }, - { name: 'Admin', _count: { role: 1 } }, - ]) - ); - - await expect( - client.user.groupBy({ - by: ['email'], - where: { - email: { not: 'u2@test.com' }, - }, - skip: 1, - take: -1, - orderBy: { email: 'desc' }, - }) - ).resolves.toEqual([{ email: 'u1@test.com' }]); - - await expect( - client.user.groupBy({ - by: ['email'], - skip: 1, - take: -2, - orderBy: { email: 'desc' }, - }) - ).resolves.toEqual([ - { email: 'u2@test.com' }, - { email: 'u1@test.com' }, - ]); - - await expect( - client.user.groupBy({ - by: ['name'], - _count: true, - having: { - name: 'User', - }, - }) - ).resolves.toEqual( - expect.arrayContaining([{ name: 'User', _count: 2 }]) - ); - - await expect( - client.user.groupBy({ - by: ['name', 'role'], - orderBy: { - _count: { - role: 'desc', - }, - }, - _count: true, - }) - ).resolves.toEqual([ - { name: 'User', role: 'USER', _count: 2 }, - { name: 'Admin', role: 'ADMIN', _count: 1 }, - ]); + }, + _count: true, + }), + ).resolves.toEqual([ + { name: 'User', role: 'USER', _count: 2 }, + { name: 'Admin', role: 'ADMIN', _count: 1 }, + ]); + }); + + it('works with multiple bys', async () => { + await createUser(client, 'u1@test.com', { + name: 'Admin1', + role: 'ADMIN', + }); + await createUser(client, 'u2@test.com', { + name: 'Admin2', + role: 'ADMIN', + }); + await createUser(client, 'u3@test.com', { + name: 'User', + role: 'USER', + }); + await createUser(client, 'u4@test.com', { + name: 'User', + role: 'USER', }); - it('works with multiple bys', async () => { - await createUser(client, 'u1@test.com', { - name: 'Admin1', - role: 'ADMIN', - }); - await createUser(client, 'u2@test.com', { - name: 'Admin2', - role: 'ADMIN', - }); - await createUser(client, 'u3@test.com', { - name: 'User', - role: 'USER', - }); - await createUser(client, 'u4@test.com', { - name: 'User', - role: 'USER', - }); - - await expect( - client.user.groupBy({ - by: ['role', 'name'], - _count: true, - }) - ).resolves.toEqual( - expect.arrayContaining([ - { role: 'ADMIN', name: 'Admin1', _count: 1 }, - { role: 'ADMIN', name: 'Admin2', _count: 1 }, - { role: 'USER', name: 'User', _count: 2 }, - ]) - ); + await expect( + client.user.groupBy({ + by: ['role', 'name'], + _count: true, + }), + ).resolves.toEqual( + expect.arrayContaining([ + { role: 'ADMIN', name: 'Admin1', _count: 1 }, + { role: 'ADMIN', name: 'Admin2', _count: 1 }, + { role: 'USER', name: 'User', _count: 2 }, + ]), + ); + }); + + it('works with different types of aggregation', async () => { + await client.profile.create({ + data: { + age: 10, + bio: 'bio', + }, + }); + await client.profile.create({ + data: { + age: 20, + bio: 'bio', + }, }); - it('works with different types of aggregation', async () => { - await client.profile.create({ - data: { - age: 10, + await expect( + client.profile.groupBy({ + by: ['bio'], + _count: { age: true }, + _avg: { age: true }, + _sum: { age: true }, + _min: { age: true }, + _max: { age: true }, + }), + ).resolves.toEqual( + expect.arrayContaining([ + { bio: 'bio', + _count: { age: 2 }, + _avg: { age: 15 }, + _sum: { age: 30 }, + _min: { age: 10 }, + _max: { age: 20 }, }, - }); - await client.profile.create({ - data: { - age: 20, - bio: 'bio', + ]), + ); + }); + + it('complains about fields in having that are not in by', async () => { + await expect( + client.profile.groupBy({ + by: ['bio'], + having: { + age: 10, }, - }); - - await expect( - client.profile.groupBy({ - by: ['bio'], - _count: { age: true }, - _avg: { age: true }, - _sum: { age: true }, - _min: { age: true }, - _max: { age: true }, - }) - ).resolves.toEqual( - expect.arrayContaining([ - { - bio: 'bio', - _count: { age: 2 }, - _avg: { age: 15 }, - _sum: { age: 30 }, - _min: { age: 10 }, - _max: { age: 20 }, - }, - ]) - ); - }); - - it('complains about fields in having that are not in by', async () => { - await expect( - client.profile.groupBy({ - by: ['bio'], - having: { - age: 10, - }, - }) - ).rejects.toThrow(/must be in \\"by\\"/); - }); - - it('complains about fields in orderBy that are not in by', async () => { - await expect( - client.profile.groupBy({ - by: ['bio'], - orderBy: { - age: 'asc', - }, - }) - ).rejects.toThrow(/must be in \\"by\\"/); - }); - } -); + }), + ).rejects.toThrow(/must be in \\"by\\"/); + }); + + it('complains about fields in orderBy that are not in by', async () => { + await expect( + client.profile.groupBy({ + by: ['bio'], + orderBy: { + age: 'asc', + }, + }), + ).rejects.toThrow(/must be in \\"by\\"/); + }); +}); diff --git a/packages/runtime/test/client-api/relation.test.ts b/packages/runtime/test/client-api/relation.test.ts index 73bf591a..04b5d36a 100644 --- a/packages/runtime/test/client-api/relation.test.ts +++ b/packages/runtime/test/client-api/relation.test.ts @@ -34,7 +34,7 @@ describe.each([ { provider, dbName: TEST_DB, - } + }, ); await expect( @@ -44,7 +44,7 @@ describe.each([ profile: { create: { age: 20 } }, }, include: { profile: true }, - }) + }), ).resolves.toMatchObject({ name: 'User', profile: { age: 20 }, @@ -73,7 +73,7 @@ describe.each([ { provider, dbName: TEST_DB, - } + }, ); await expect( @@ -84,7 +84,7 @@ describe.each([ profile2: { create: { age: 21 } }, }, include: { profile1: true, profile2: true }, - }) + }), ).resolves.toMatchObject({ name: 'User', profile1: { age: 20 }, @@ -111,7 +111,7 @@ describe.each([ { provider, dbName: TEST_DB, - } + }, ); await expect( @@ -123,13 +123,10 @@ describe.each([ }, }, include: { posts: true }, - }) + }), ).resolves.toMatchObject({ name: 'User', - posts: [ - expect.objectContaining({ title: 'Post 1' }), - expect.objectContaining({ title: 'Post 2' }), - ], + posts: [expect.objectContaining({ title: 'Post 1' }), expect.objectContaining({ title: 'Post 2' })], }); }); @@ -155,7 +152,7 @@ describe.each([ { provider, dbName: TEST_DB, - } + }, ); await expect( @@ -170,17 +167,11 @@ describe.each([ }, }, include: { posts1: true, posts2: true }, - }) + }), ).resolves.toMatchObject({ name: 'User', - posts1: [ - expect.objectContaining({ title: 'Post 1' }), - expect.objectContaining({ title: 'Post 2' }), - ], - posts2: [ - expect.objectContaining({ title: 'Post 3' }), - expect.objectContaining({ title: 'Post 4' }), - ], + posts1: [expect.objectContaining({ title: 'Post 1' }), expect.objectContaining({ title: 'Post 2' })], + posts2: [expect.objectContaining({ title: 'Post 3' }), expect.objectContaining({ title: 'Post 4' })], }); }); @@ -211,7 +202,7 @@ describe.each([ { provider, dbName: TEST_DB, - } + }, ); await client.user.create({ data: { id: 1, name: 'User1' } }); @@ -226,7 +217,7 @@ describe.each([ await expect( client.user.findMany({ include: { tags: { include: { tag: true } } }, - }) + }), ).resolves.toMatchObject([ expect.objectContaining({ name: 'User1', @@ -260,17 +251,13 @@ describe.each([ id Int @id @default(autoincrement()) name String profile Profile? - tags Tag[] ${ - relationName ? `@relation("${relationName}")` : '' - } + tags Tag[] ${relationName ? `@relation("${relationName}")` : ''} } model Tag { id Int @id @default(autoincrement()) name String - users User[] ${ - relationName ? `@relation("${relationName}")` : '' - } + users User[] ${relationName ? `@relation("${relationName}")` : ''} } model Profile { @@ -282,10 +269,9 @@ describe.each([ `, { provider, - dbName: - provider === 'sqlite' ? 'file:./dev.db' : TEST_DB, + dbName: provider === 'sqlite' ? 'file:./dev.db' : TEST_DB, usePrismaPush: true, - } + }, ); }); @@ -320,12 +306,9 @@ describe.each([ await expect( client.user.findFirst({ include: { tags: true }, - }) + }), ).resolves.toMatchObject({ - tags: [ - expect.objectContaining({ name: 'Tag1' }), - expect.objectContaining({ name: 'Tag2' }), - ], + tags: [expect.objectContaining({ name: 'Tag1' }), expect.objectContaining({ name: 'Tag2' })], }); await expect( @@ -335,13 +318,10 @@ describe.each([ include: { tags: true }, }, }, - }) + }), ).resolves.toMatchObject({ user: expect.objectContaining({ - tags: [ - expect.objectContaining({ name: 'Tag1' }), - expect.objectContaining({ name: 'Tag2' }), - ], + tags: [expect.objectContaining({ name: 'Tag1' }), expect.objectContaining({ name: 'Tag2' })], }), }); @@ -349,7 +329,7 @@ describe.each([ client.user.findUnique({ where: { id: 2 }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [], }); @@ -359,7 +339,7 @@ describe.each([ client.user.findFirst({ where: { id: 1 }, include: { tags: { where: { name: 'Tag1' } } }, - }) + }), ).resolves.toMatchObject({ tags: [expect.objectContaining({ name: 'Tag1' })], }); @@ -368,7 +348,7 @@ describe.each([ await expect( client.user.findMany({ where: { tags: { some: { name: 'Tag1' } } }, - }) + }), ).resolves.toEqual([ expect.objectContaining({ name: 'User1', @@ -377,7 +357,7 @@ describe.each([ await expect( client.user.findMany({ where: { tags: { none: { name: 'Tag1' } } }, - }) + }), ).resolves.toEqual([ expect.objectContaining({ name: 'User2', @@ -406,12 +386,9 @@ describe.each([ }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ - tags: [ - expect.objectContaining({ name: 'Tag1' }), - expect.objectContaining({ name: 'Tag2' }), - ], + tags: [expect.objectContaining({ name: 'Tag1' }), expect.objectContaining({ name: 'Tag2' })], }); // connect @@ -423,7 +400,7 @@ describe.each([ tags: { connect: { id: 1 } }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [expect.objectContaining({ name: 'Tag1' })], }); @@ -442,7 +419,7 @@ describe.each([ }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [expect.objectContaining({ id: 1, name: 'Tag1' })], }); @@ -460,7 +437,7 @@ describe.each([ }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [expect.objectContaining({ id: 3, name: 'Tag3' })], }); @@ -499,12 +476,9 @@ describe.each([ }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ - tags: [ - expect.objectContaining({ id: 1 }), - expect.objectContaining({ id: 2 }), - ], + tags: [expect.objectContaining({ id: 1 }), expect.objectContaining({ id: 2 })], }); await client.tag.create({ @@ -520,7 +494,7 @@ describe.each([ where: { id: 1 }, data: { tags: { connect: { id: 3 } } }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [ expect.objectContaining({ id: 1 }), @@ -534,7 +508,7 @@ describe.each([ where: { id: 1 }, data: { tags: { connect: { id: 3 } } }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [ expect.objectContaining({ id: 1 }), @@ -551,7 +525,7 @@ describe.each([ tags: { disconnect: { id: 3, name: 'not found' } }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [ expect.objectContaining({ id: 1 }), @@ -566,23 +540,18 @@ describe.each([ where: { id: 1 }, data: { tags: { disconnect: { id: 3 } } }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ - tags: [ - expect.objectContaining({ id: 1 }), - expect.objectContaining({ id: 2 }), - ], + tags: [expect.objectContaining({ id: 1 }), expect.objectContaining({ id: 2 })], }); await expect( client.$qbRaw - .selectFrom( - relationName ? `_${relationName}` : '_TagToUser' - ) + .selectFrom(relationName ? `_${relationName}` : '_TagToUser') .selectAll() .where('B', '=', 1) // user id .where('A', '=', 3) // tag id - .execute() + .execute(), ).resolves.toHaveLength(0); await expect( @@ -590,12 +559,9 @@ describe.each([ where: { id: 1 }, data: { tags: { set: [{ id: 2 }, { id: 3 }] } }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ - tags: [ - expect.objectContaining({ id: 2 }), - expect.objectContaining({ id: 3 }), - ], + tags: [expect.objectContaining({ id: 2 }), expect.objectContaining({ id: 3 })], }); // update - not found @@ -610,7 +576,7 @@ describe.each([ }, }, }, - }) + }), ).toBeRejectedNotFound(); // update - found @@ -626,7 +592,7 @@ describe.each([ }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: expect.arrayContaining([ expect.objectContaining({ @@ -650,7 +616,7 @@ describe.each([ }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [ expect.objectContaining({ @@ -664,9 +630,7 @@ describe.each([ ], }); - await expect( - client.tag.findUnique({ where: { id: 1 } }) - ).resolves.toMatchObject({ + await expect(client.tag.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ name: 'Tag1', }); @@ -684,7 +648,7 @@ describe.each([ }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [ expect.objectContaining({ @@ -712,11 +676,9 @@ describe.each([ }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ - tags: expect.arrayContaining([ - expect.objectContaining({ id: 4, name: 'Tag4' }), - ]), + tags: expect.arrayContaining([expect.objectContaining({ id: 4, name: 'Tag4' })]), }); // delete - not found @@ -724,7 +686,7 @@ describe.each([ client.user.update({ where: { id: 1 }, data: { tags: { delete: { id: 1 } } }, - }) + }), ).toBeRejectedNotFound(); // delete - found @@ -733,16 +695,11 @@ describe.each([ where: { id: 1 }, data: { tags: { delete: { id: 2 } } }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ - tags: [ - expect.objectContaining({ id: 3 }), - expect.objectContaining({ id: 4 }), - ], + tags: [expect.objectContaining({ id: 3 }), expect.objectContaining({ id: 4 })], }); - await expect( - client.tag.findUnique({ where: { id: 2 } }) - ).toResolveNull(); + await expect(client.tag.findUnique({ where: { id: 2 } })).toResolveNull(); // deleteMany await expect( @@ -752,16 +709,12 @@ describe.each([ tags: { deleteMany: { id: { in: [1, 2, 3] } } }, }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [expect.objectContaining({ id: 4 })], }); - await expect( - client.tag.findUnique({ where: { id: 3 } }) - ).toResolveNull(); - await expect( - client.tag.findUnique({ where: { id: 1 } }) - ).toResolveTruthy(); + await expect(client.tag.findUnique({ where: { id: 3 } })).toResolveNull(); + await expect(client.tag.findUnique({ where: { id: 1 } })).toResolveTruthy(); }); it('works with delete', async () => { @@ -786,7 +739,7 @@ describe.each([ client.user.findUnique({ where: { id: 1 }, include: { tags: true }, - }) + }), ).resolves.toMatchObject({ tags: [expect.objectContaining({ id: 2 })], }); @@ -799,11 +752,11 @@ describe.each([ client.tag.findUnique({ where: { id: 2 }, include: { users: true }, - }) + }), ).resolves.toMatchObject({ users: [], }); }); - } + }, ); }); diff --git a/packages/runtime/test/client-api/scalar-list.test.ts b/packages/runtime/test/client-api/scalar-list.test.ts index b031ab83..b10744e5 100644 --- a/packages/runtime/test/client-api/scalar-list.test.ts +++ b/packages/runtime/test/client-api/scalar-list.test.ts @@ -32,7 +32,7 @@ describe('Scalar list tests', () => { data: { name: 'user', }, - }) + }), ).resolves.toMatchObject({ tags: [], }); @@ -43,7 +43,7 @@ describe('Scalar list tests', () => { name: 'user', tags: [], }, - }) + }), ).resolves.toMatchObject({ tags: [], }); @@ -54,7 +54,7 @@ describe('Scalar list tests', () => { name: 'user', tags: ['tag1', 'tag2'], }, - }) + }), ).resolves.toMatchObject({ tags: ['tag1', 'tag2'], }); @@ -65,7 +65,7 @@ describe('Scalar list tests', () => { name: 'user', tags: { set: ['tag1', 'tag2'] }, }, - }) + }), ).resolves.toMatchObject({ tags: ['tag1', 'tag2'], }); @@ -76,7 +76,7 @@ describe('Scalar list tests', () => { name: 'user', flags: [true, false], }, - }) + }), ).resolves.toMatchObject({ flags: [true, false] }); await expect( @@ -85,7 +85,7 @@ describe('Scalar list tests', () => { name: 'user', flags: { set: [true, false] }, }, - }) + }), ).resolves.toMatchObject({ flags: [true, false] }); }); @@ -101,42 +101,42 @@ describe('Scalar list tests', () => { client.user.update({ where: { id: user.id }, data: { tags: ['tag3', 'tag4'] }, - }) + }), ).resolves.toMatchObject({ tags: ['tag3', 'tag4'] }); await expect( client.user.update({ where: { id: user.id }, data: { tags: { set: ['tag5'] } }, - }) + }), ).resolves.toMatchObject({ tags: ['tag5'] }); await expect( client.user.update({ where: { id: user.id }, data: { tags: { push: 'tag6' } }, - }) + }), ).resolves.toMatchObject({ tags: ['tag5', 'tag6'] }); await expect( client.user.update({ where: { id: user.id }, data: { tags: { push: [] } }, - }) + }), ).resolves.toMatchObject({ tags: ['tag5', 'tag6'] }); await expect( client.user.update({ where: { id: user.id }, data: { tags: { push: ['tag7', 'tag8'] } }, - }) + }), ).resolves.toMatchObject({ tags: ['tag5', 'tag6', 'tag7', 'tag8'] }); await expect( client.user.update({ where: { id: user.id }, data: { tags: { set: [] } }, - }) + }), ).resolves.toMatchObject({ tags: [] }); }); @@ -147,8 +147,7 @@ describe('Scalar list tests', () => { tags: ['tag1', 'tag2'], }, }); - // @ts-ignore - const user2 = await client.user.create({ + await client.user.create({ data: { name: 'user2', }, @@ -163,73 +162,73 @@ describe('Scalar list tests', () => { await expect( client.user.findMany({ where: { tags: { equals: ['tag1', 'tag2'] } }, - }) + }), ).resolves.toMatchObject([user1]); await expect( client.user.findFirst({ where: { tags: { equals: ['tag1'] } }, - }) + }), ).toResolveNull(); await expect( client.user.findMany({ where: { tags: { has: 'tag1' } }, - }) + }), ).resolves.toMatchObject([user1]); await expect( client.user.findFirst({ where: { tags: { has: 'tag3' } }, - }) + }), ).toResolveNull(); await expect( client.user.findMany({ where: { tags: { hasSome: ['tag1'] } }, - }) + }), ).resolves.toMatchObject([user1]); await expect( client.user.findMany({ where: { tags: { hasSome: ['tag1', 'tag3'] } }, - }) + }), ).resolves.toMatchObject([user1]); await expect( client.user.findFirst({ where: { tags: { hasSome: [] } }, - }) + }), ).toResolveNull(); await expect( client.user.findFirst({ where: { tags: { hasEvery: ['tag3', 'tag4'] } }, - }) + }), ).toResolveNull(); await expect( client.user.findMany({ where: { tags: { hasEvery: ['tag1', 'tag2'] } }, - }) + }), ).resolves.toMatchObject([user1]); await expect( client.user.findFirst({ where: { tags: { hasEvery: ['tag1', 'tag3'] } }, - }) + }), ).toResolveNull(); await expect( client.user.findMany({ where: { tags: { isEmpty: true } }, - }) + }), ).resolves.toEqual([user3]); await expect( client.user.findMany({ where: { tags: { isEmpty: false } }, - }) + }), ).resolves.toEqual([user1]); }); }); diff --git a/packages/runtime/test/client-api/type-coverage.test.ts b/packages/runtime/test/client-api/type-coverage.test.ts index 9ad08705..3574d49d 100644 --- a/packages/runtime/test/client-api/type-coverage.test.ts +++ b/packages/runtime/test/client-api/type-coverage.test.ts @@ -20,7 +20,7 @@ describe('zmodel type coverage tests', () => { @@allow('all', true) } - ` + `, ); const date = new Date(); diff --git a/packages/runtime/test/client-api/update-many.test.ts b/packages/runtime/test/client-api/update-many.test.ts index cb440818..e8d08cd9 100644 --- a/packages/runtime/test/client-api/update-many.test.ts +++ b/packages/runtime/test/client-api/update-many.test.ts @@ -5,116 +5,97 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-update-many-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client updateMany tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client updateMany tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); - }); + beforeEach(async () => { + client = await createClient(); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); - afterEach(async () => { - await client?.$disconnect(); + it('works with toplevel updateMany', async () => { + // nothing to update + await expect(client.user.updateMany({ data: {} })).resolves.toMatchObject({ + count: 0, }); - it('works with toplevel updateMany', async () => { - // nothing to update - await expect( - client.user.updateMany({ data: {} }) - ).resolves.toMatchObject({ - count: 0, - }); + // nothing to update + await expect(client.user.updateMany({ data: { name: 'Foo' } })).resolves.toMatchObject({ + count: 0, + }); - // nothing to update - await expect( - client.user.updateMany({ data: { name: 'Foo' } }) - ).resolves.toMatchObject({ - count: 0, - }); + await client.user.create({ + data: { id: '1', email: 'u1@test.com', name: 'User1' }, + }); + await client.user.create({ + data: { id: '2', email: 'u2@test.com', name: 'User2' }, + }); - await client.user.create({ - data: { id: '1', email: 'u1@test.com', name: 'User1' }, - }); - await client.user.create({ - data: { id: '2', email: 'u2@test.com', name: 'User2' }, - }); + // no matching + await expect( + client.user.updateMany({ + where: { email: 'foo' }, + data: { name: 'Foo' }, + }), + ).resolves.toMatchObject({ count: 0 }); + await expect(client.user.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ name: 'User1' }); - // no matching - await expect( - client.user.updateMany({ - where: { email: 'foo' }, - data: { name: 'Foo' }, - }) - ).resolves.toMatchObject({ count: 0 }); - await expect( - client.user.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ name: 'User1' }); + // match all + await expect( + client.user.updateMany({ + data: { name: 'Foo' }, + }), + ).resolves.toMatchObject({ count: 2 }); + await expect(client.user.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ name: 'Foo' }); + await expect(client.user.findUnique({ where: { id: '2' } })).resolves.toMatchObject({ name: 'Foo' }); - // match all - await expect( - client.user.updateMany({ - data: { name: 'Foo' }, - }) - ).resolves.toMatchObject({ count: 2 }); - await expect( - client.user.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ name: 'Foo' }); - await expect( - client.user.findUnique({ where: { id: '2' } }) - ).resolves.toMatchObject({ name: 'Foo' }); + // match one + await expect( + client.user.updateMany({ + where: { id: '1' }, + data: { name: 'Bar' }, + }), + ).resolves.toMatchObject({ count: 1 }); + await expect(client.user.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ name: 'Bar' }); + await expect(client.user.findUnique({ where: { id: '2' } })).resolves.toMatchObject({ name: 'Foo' }); - // match one - await expect( - client.user.updateMany({ - where: { id: '1' }, - data: { name: 'Bar' }, - }) - ).resolves.toMatchObject({ count: 1 }); - await expect( - client.user.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ name: 'Bar' }); - await expect( - client.user.findUnique({ where: { id: '2' } }) - ).resolves.toMatchObject({ name: 'Foo' }); + // limit + await expect( + client.user.updateMany({ + data: { name: 'Baz' }, + limit: 1, + }), + ).resolves.toMatchObject({ count: 1 }); + await expect(client.user.findMany({ where: { name: 'Baz' } })).toResolveWithLength(1); - // limit - await expect( - client.user.updateMany({ - data: { name: 'Baz' }, - limit: 1, - }) - ).resolves.toMatchObject({ count: 1 }); - await expect( - client.user.findMany({ where: { name: 'Baz' } }) - ).toResolveWithLength(1); + // limit with where + await expect( + client.user.updateMany({ + where: { name: 'Zee' }, + data: { name: 'Baz' }, + limit: 1, + }), + ).resolves.toMatchObject({ count: 0 }); + }); - // limit with where - await expect( - client.user.updateMany({ - where: { name: 'Zee' }, - data: { name: 'Baz' }, - limit: 1, - }) - ).resolves.toMatchObject({ count: 0 }); + it('works with updateManyAndReturn', async () => { + await client.user.create({ + data: { id: '1', email: 'u1@test.com', name: 'User1' }, + }); + await client.user.create({ + data: { id: '2', email: 'u2@test.com', name: 'User2' }, }); - it('works with updateManyAndReturn', async () => { - await client.user.create({ - data: { id: '1', email: 'u1@test.com', name: 'User1' }, - }); - await client.user.create({ - data: { id: '2', email: 'u2@test.com', name: 'User2' }, - }); - - const r = await client.user.updateManyAndReturn({ - where: { email: 'u1@test.com' }, - data: { name: 'User1-new' }, - select: { id: true, name: true }, - }); - expect(r).toMatchObject([{ id: '1', name: 'User1-new' }]); - // @ts-expect-error - expect(r[0]!.email).toBeUndefined(); + const r = await client.user.updateManyAndReturn({ + where: { email: 'u1@test.com' }, + data: { name: 'User1-new' }, + select: { id: true, name: true }, }); - } -); + expect(r).toMatchObject([{ id: '1', name: 'User1-new' }]); + // @ts-expect-error + expect(r[0]!.email).toBeUndefined(); + }); +}); diff --git a/packages/runtime/test/client-api/update.test.ts b/packages/runtime/test/client-api/update.test.ts index 5fc8d667..b65aa501 100644 --- a/packages/runtime/test/client-api/update.test.ts +++ b/packages/runtime/test/client-api/update.test.ts @@ -6,2133 +6,2055 @@ import { createUser } from './utils'; const PG_DB_NAME = 'client-api-update-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client update tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client update tests', ({ createClient }) => { + let client: ClientContract; + + beforeEach(async () => { + client = await createClient(); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); + + describe('toplevel', () => { + it('works with toplevel update', async () => { + const user = await createUser(client, 'u1@test.com'); + + expect(user.updatedAt).toBeInstanceOf(Date); + + // not found + await expect( + client.user.update({ + where: { id: 'not-found' }, + data: { name: 'Foo' }, + }), + ).toBeRejectedNotFound(); + + // empty data + let updated = await client.user.update({ + where: { id: user.id }, + data: {}, + }); + expect(updated).toMatchObject({ + email: user.email, + name: user.name, + }); + expect(updated.updatedAt.getTime()).toBeGreaterThan(user.updatedAt.getTime()); - beforeEach(async () => { - client = await createClient(); - }); + // id as filter + updated = await client.user.update({ + where: { id: user.id }, + data: { email: 'u2.test.com', name: 'Foo' }, + }); + expect(updated).toMatchObject({ + email: 'u2.test.com', + name: 'Foo', + }); + expect(updated.updatedAt.getTime()).toBeGreaterThan(user.updatedAt.getTime()); + + // non-id unique as filter + await expect( + client.user.update({ + where: { email: 'u2.test.com' }, + data: { email: 'u2.test.com', name: 'Bar' }, + }), + ).resolves.toMatchObject({ + email: 'u2.test.com', + name: 'Bar', + }); - afterEach(async () => { - await client?.$disconnect(); + // select + await expect( + client.user.update({ + where: { id: user.id }, + data: { email: 'u2.test.com', name: 'Bar1' }, + select: { email: true, name: true }, + }), + ).resolves.toEqual({ email: 'u2.test.com', name: 'Bar1' }); + + // include + const r = await client.user.update({ + where: { id: user.id }, + data: { email: 'u2.test.com', name: 'Bar2' }, + include: { profile: true }, + }); + expect(r.profile).toBeTruthy(); + expect(r.email).toBeTruthy(); + + // include + select + await expect( + client.user.update({ + where: { id: user.id }, + data: { email: 'u2.test.com', name: 'Bar3' }, + include: { profile: true }, + select: { email: true, name: true }, + } as any), + ).rejects.toThrow('cannot be used together'); + + // update with non-unique filter + await expect( + client.user.update({ + // @ts-expect-error + where: { name: 'Foo' }, + data: { name: 'Bar' }, + }), + ).rejects.toThrow('At least one unique field or field set must be set'); + await expect( + client.user.update({ + where: { id: undefined }, + data: { name: 'Bar' }, + }), + ).rejects.toThrow('At least one unique field or field set must be set'); + + // id update + await expect( + client.user.update({ + where: { id: user.id }, + data: { id: 'user2' }, + }), + ).resolves.toMatchObject({ id: 'user2' }); }); - describe('toplevel', () => { - it('works with toplevel update', async () => { - const user = await createUser(client, 'u1@test.com'); + it('works with numeric incremental update', async () => { + await createUser(client, 'u1@test.com', { + profile: { create: { id: '1', bio: 'bio' } }, + }); - expect(user.updatedAt).toBeInstanceOf(Date); + await expect( + client.profile.update({ + where: { id: '1' }, + data: { age: { increment: 1 } }, + }), + ).resolves.toMatchObject({ age: null }); + + await expect( + client.profile.update({ + where: { id: '1' }, + data: { age: { set: 1 } }, + }), + ).resolves.toMatchObject({ age: 1 }); + + await expect( + client.profile.update({ + where: { id: '1' }, + data: { age: { increment: 1 } }, + }), + ).resolves.toMatchObject({ age: 2 }); + + await expect( + client.profile.update({ + where: { id: '1' }, + data: { age: { multiply: 2 } }, + }), + ).resolves.toMatchObject({ age: 4 }); + + await expect( + client.profile.update({ + where: { id: '1' }, + data: { age: { divide: 2 } }, + }), + ).resolves.toMatchObject({ age: 2 }); + + await expect( + client.profile.update({ + where: { id: '1' }, + data: { age: { decrement: 1 } }, + }), + ).resolves.toMatchObject({ age: 1 }); + + await expect( + client.profile.update({ + where: { id: '1' }, + data: { age: { set: null } }, + }), + ).resolves.toMatchObject({ age: null }); + }); + }); - // not found - await expect( - client.user.update({ - where: { id: 'not-found' }, - data: { name: 'Foo' }, - }) - ).toBeRejectedNotFound(); + describe('nested to-many', () => { + it('works with nested to-many relation simple create', async () => { + const user = await createUser(client, 'u1@test.com'); - // empty data - let updated = await client.user.update({ + // create + await expect( + client.user.update({ where: { id: user.id }, - data: {}, - }); - expect(updated).toMatchObject({ - email: user.email, - name: user.name, - }); - expect(updated.updatedAt.getTime()).toBeGreaterThan( - user.updatedAt.getTime() - ); - - // id as filter - updated = await client.user.update({ + data: { + posts: { create: { id: '1', title: 'Post1' } }, + }, + include: { posts: true }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ id: '1', title: 'Post1' })], + }); + + // create multiple + await expect( + client.user.update({ where: { id: user.id }, - data: { email: 'u2.test.com', name: 'Foo' }, - }); - expect(updated).toMatchObject({ - email: 'u2.test.com', - name: 'Foo', - }); - expect(updated.updatedAt.getTime()).toBeGreaterThan( - user.updatedAt.getTime() - ); - - // non-id unique as filter - await expect( - client.user.update({ - where: { email: 'u2.test.com' }, - data: { email: 'u2.test.com', name: 'Bar' }, - }) - ).resolves.toMatchObject({ - email: 'u2.test.com', - name: 'Bar', - }); - - // select - await expect( - client.user.update({ - where: { id: user.id }, - data: { email: 'u2.test.com', name: 'Bar1' }, - select: { email: true, name: true }, - }) - ).resolves.toEqual({ email: 'u2.test.com', name: 'Bar1' }); - - // include - const r = await client.user.update({ + data: { + posts: { + create: [ + { id: '2', title: 'Post2' }, + { id: '3', title: 'Post3' }, + ], + }, + }, + include: { posts: true }, + }), + ).resolves.toSatisfy((r) => r.posts.length === 3); + }); + + it('works with nested to-many relation createMany', async () => { + const user = await createUser(client, 'u1@test.com'); + + // single + await expect( + client.user.update({ where: { id: user.id }, - data: { email: 'u2.test.com', name: 'Bar2' }, - include: { profile: true }, - }); - expect(r.profile).toBeTruthy(); - expect(r.email).toBeTruthy(); - - // include + select - await expect( - client.user.update({ - where: { id: user.id }, - data: { email: 'u2.test.com', name: 'Bar3' }, - include: { profile: true }, - select: { email: true, name: true }, - } as any) - ).rejects.toThrow('cannot be used together'); - - // update with non-unique filter - await expect( - client.user.update({ - // @ts-expect-error - where: { name: 'Foo' }, - data: { name: 'Bar' }, - }) - ).rejects.toThrow( - 'At least one unique field or field set must be set' - ); - await expect( - client.user.update({ - where: { id: undefined }, - data: { name: 'Bar' }, - }) - ).rejects.toThrow( - 'At least one unique field or field set must be set' - ); - - // id update - await expect( - client.user.update({ - where: { id: user.id }, - data: { id: 'user2' }, - }) - ).resolves.toMatchObject({ id: 'user2' }); - }); - - it('works with numeric incremental update', async () => { - await createUser(client, 'u1@test.com', { - profile: { create: { id: '1', bio: 'bio' } }, - }); - - await expect( - client.profile.update({ - where: { id: '1' }, - data: { age: { increment: 1 } }, - }) - ).resolves.toMatchObject({ age: null }); - - await expect( - client.profile.update({ - where: { id: '1' }, - data: { age: { set: 1 } }, - }) - ).resolves.toMatchObject({ age: 1 }); - - await expect( - client.profile.update({ - where: { id: '1' }, - data: { age: { increment: 1 } }, - }) - ).resolves.toMatchObject({ age: 2 }); - - await expect( - client.profile.update({ - where: { id: '1' }, - data: { age: { multiply: 2 } }, - }) - ).resolves.toMatchObject({ age: 4 }); - - await expect( - client.profile.update({ - where: { id: '1' }, - data: { age: { divide: 2 } }, - }) - ).resolves.toMatchObject({ age: 2 }); - - await expect( - client.profile.update({ - where: { id: '1' }, - data: { age: { decrement: 1 } }, - }) - ).resolves.toMatchObject({ age: 1 }); - - await expect( - client.profile.update({ - where: { id: '1' }, - data: { age: { set: null } }, - }) - ).resolves.toMatchObject({ age: null }); + data: { + posts: { + createMany: { + data: { id: '1', title: 'Post1' }, + }, + }, + }, + include: { posts: true }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ id: '1', title: 'Post1' })], }); - }); - describe('nested to-many', () => { - it('works with nested to-many relation simple create', async () => { - const user = await createUser(client, 'u1@test.com'); - - // create - await expect( - client.user.update({ - where: { id: user.id }, - data: { - posts: { create: { id: '1', title: 'Post1' } }, - }, - include: { posts: true }, - }) - ).resolves.toMatchObject({ - posts: [ - expect.objectContaining({ id: '1', title: 'Post1' }), - ], - }); - - // create multiple - await expect( - client.user.update({ - where: { id: user.id }, - data: { - posts: { - create: [ + // multiple + await expect( + client.user.update({ + where: { id: user.id }, + data: { + posts: { + createMany: { + data: [ + { id: '1', title: 'Post1' }, { id: '2', title: 'Post2' }, { id: '3', title: 'Post3' }, ], + skipDuplicates: true, }, }, - include: { posts: true }, - }) - ).resolves.toSatisfy((r) => r.posts.length === 3); - }); - - it('works with nested to-many relation createMany', async () => { - const user = await createUser(client, 'u1@test.com'); + }, + include: { posts: true }, + }), + ).resolves.toSatisfy((r) => r.posts.length === 3); - // single - await expect( - client.user.update({ - where: { id: user.id }, - data: { - posts: { - createMany: { - data: { id: '1', title: 'Post1' }, - }, - }, - }, - include: { posts: true }, - }) - ).resolves.toMatchObject({ - posts: [ - expect.objectContaining({ id: '1', title: 'Post1' }), - ], - }); - - // multiple - await expect( - client.user.update({ - where: { id: user.id }, - data: { - posts: { - createMany: { - data: [ - { id: '1', title: 'Post1' }, - { id: '2', title: 'Post2' }, - { id: '3', title: 'Post3' }, - ], - skipDuplicates: true, - }, - }, - }, - include: { posts: true }, - }) - ).resolves.toSatisfy((r) => r.posts.length === 3); - - // duplicate id - await expect( - client.user.update({ - where: { id: user.id }, - data: { - posts: { - createMany: { - data: { id: '1', title: 'Post1-1' }, - }, + // duplicate id + await expect( + client.user.update({ + where: { id: user.id }, + data: { + posts: { + createMany: { + data: { id: '1', title: 'Post1-1' }, }, }, - }) - ).rejects.toThrow(); - - // duplicate id - await expect( - client.user.update({ - where: { id: user.id }, - data: { - posts: { - createMany: { - data: [ - { id: '4', title: 'Post4' }, - { id: '4', title: 'Post4-1' }, - ], - }, + }, + }), + ).rejects.toThrow(); + + // duplicate id + await expect( + client.user.update({ + where: { id: user.id }, + data: { + posts: { + createMany: { + data: [ + { id: '4', title: 'Post4' }, + { id: '4', title: 'Post4-1' }, + ], }, }, - }) - ).rejects.toThrow(); + }, + }), + ).rejects.toThrow(); + }); + + it('works with nested to-many relation set', async () => { + const user = await createUser(client, 'u1@test.com'); + + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + comments: { + create: [ + { id: '1', content: 'Comment1' }, + { id: '2', content: 'Comment2' }, + ], + }, + }, }); - it('works with nested to-many relation set', async () => { - const user = await createUser(client, 'u1@test.com'); + // set empty + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { set: [] } }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ comments: [] }); + + // set single + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { set: { id: '1' } } }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: '1' })], + }); + await client.post.update({ + where: { id: post.id }, + data: { comments: { set: [] } }, + }); - const post = await client.post.create({ + // non-existing + await expect( + client.post.update({ + where: { id: post.id }, data: { - title: 'Post1', - author: { connect: { id: user.id } }, comments: { - create: [ - { id: '1', content: 'Comment1' }, - { id: '2', content: 'Comment2' }, + set: [ + { id: '1' }, + { id: '2' }, + { id: '3' }, // non-existing ], }, }, - }); - - // set empty - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { set: [] } }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ comments: [] }); - - // set single - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { set: { id: '1' } } }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [expect.objectContaining({ id: '1' })], - }); - await client.post.update({ + include: { comments: true }, + }), + ).toBeRejectedNotFound(); + + // set multiple + await expect( + client.post.update({ where: { id: post.id }, - data: { comments: { set: [] } }, - }); - - // non-existing - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - set: [ - { id: '1' }, - { id: '2' }, - { id: '3' }, // non-existing - ], - }, + data: { + comments: { + set: [{ id: '1' }, { id: '2' }], }, - include: { comments: true }, - }) - ).toBeRejectedNotFound(); - - // set multiple - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - set: [{ id: '1' }, { id: '2' }], - }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: '1' }), expect.objectContaining({ id: '2' })], + }); + }); + + it('works with nested to-many relation simple connect', async () => { + const user = await createUser(client, 'u1@test.com'); + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + }, + }); + const comment1 = await client.comment.create({ + data: { id: '1', content: 'Comment1' }, + }); + const comment2 = await client.comment.create({ + data: { id: '2', content: 'Comment2' }, + }); + + // connect single + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { connect: { id: comment1.id } } }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: comment1.id })], + }); + + // already connected + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { connect: { id: comment1.id } } }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: comment1.id })], + }); + + // connect non existing + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + connect: [ + { id: comment1.id }, + { id: comment2.id }, + { id: '3' }, // non-existing + ], }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: '1' }), - expect.objectContaining({ id: '2' }), - ], - }); - }); - - it('works with nested to-many relation simple connect', async () => { - const user = await createUser(client, 'u1@test.com'); - const post = await client.post.create({ - data: { - title: 'Post1', - author: { connect: { id: user.id } }, - }, - }); - const comment1 = await client.comment.create({ - data: { id: '1', content: 'Comment1' }, - }); - const comment2 = await client.comment.create({ - data: { id: '2', content: 'Comment2' }, - }); - - // connect single - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { connect: { id: comment1.id } } }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [expect.objectContaining({ id: comment1.id })], - }); - - // already connected - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { connect: { id: comment1.id } } }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [expect.objectContaining({ id: comment1.id })], - }); - - // connect non existing - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - connect: [ - { id: comment1.id }, - { id: comment2.id }, - { id: '3' }, // non-existing - ], - }, + }, + include: { comments: true }, + }), + ).toBeRejectedNotFound(); + + // connect multiple + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + connect: [{ id: comment1.id }, { id: comment2.id }], }, - include: { comments: true }, - }) - ).toBeRejectedNotFound(); - - // connect multiple - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - connect: [ - { id: comment1.id }, - { id: comment2.id }, - ], + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: comment1.id }), expect.objectContaining({ id: comment2.id })], + }); + }); + + it('works with nested to-many relation connectOrCreate', async () => { + const user = await createUser(client, 'u1@test.com'); + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + }, + }); + const comment1 = await client.comment.create({ + data: { id: '1', content: 'Comment1' }, + }); + const comment2 = await client.comment.create({ + data: { id: '2', content: 'Comment2' }, + }); + + // single + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + connectOrCreate: { + where: { + id: comment1.id, + }, + create: { content: 'Comment1' }, }, }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: comment1.id }), - expect.objectContaining({ id: comment2.id }), - ], - }); - }); - - it('works with nested to-many relation connectOrCreate', async () => { - const user = await createUser(client, 'u1@test.com'); - const post = await client.post.create({ - data: { - title: 'Post1', - author: { connect: { id: user.id } }, - }, - }); - const comment1 = await client.comment.create({ - data: { id: '1', content: 'Comment1' }, - }); - const comment2 = await client.comment.create({ - data: { id: '2', content: 'Comment2' }, - }); - - // single - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - connectOrCreate: { - where: { - id: comment1.id, - }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: comment1.id })], + }); + + // multiple + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + connectOrCreate: [ + { + // already connected + where: { id: comment1.id }, create: { content: 'Comment1' }, }, - }, - }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [expect.objectContaining({ id: comment1.id })], - }); - - // multiple - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - connectOrCreate: [ - { - // already connected - where: { id: comment1.id }, - create: { content: 'Comment1' }, - }, - { - // not connected - where: { id: comment2.id }, - create: { content: 'Comment2' }, - }, - { - // create - where: { id: '3' }, - create: { - id: '3', - content: 'Comment3', - }, + { + // not connected + where: { id: comment2.id }, + create: { content: 'Comment2' }, + }, + { + // create + where: { id: '3' }, + create: { + id: '3', + content: 'Comment3', }, - ], - }, + }, + ], }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: comment1.id }), - expect.objectContaining({ id: comment2.id }), - expect.objectContaining({ id: '3' }), - ], - }); + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [ + expect.objectContaining({ id: comment1.id }), + expect.objectContaining({ id: comment2.id }), + expect.objectContaining({ id: '3' }), + ], }); + }); - it('works with nested to-many relation disconnect', async () => { - const user = await createUser(client, 'u1@test.com'); - const post = await client.post.create({ + it('works with nested to-many relation disconnect', async () => { + const user = await createUser(client, 'u1@test.com'); + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + comments: { + create: [ + { id: '1', content: 'Comment1' }, + { id: '2', content: 'Comment2' }, + { id: '3', content: 'Comment3' }, + ], + }, + }, + }); + + // single + await expect( + client.post.update({ + where: { id: post.id }, data: { - title: 'Post1', - author: { connect: { id: user.id } }, comments: { - create: [ - { id: '1', content: 'Comment1' }, - { id: '2', content: 'Comment2' }, - { id: '3', content: 'Comment3' }, - ], + disconnect: { id: '1', content: 'non found' }, }, }, - }); + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [ + expect.objectContaining({ id: '1' }), + expect.objectContaining({ id: '2' }), + expect.objectContaining({ id: '3' }), + ], + }); + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { disconnect: { id: '1' } } }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: '2' }), expect.objectContaining({ id: '3' })], + }); - // single - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - disconnect: { id: '1', content: 'non found' }, - }, - }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: '1' }), - expect.objectContaining({ id: '2' }), - expect.objectContaining({ id: '3' }), - ], - }); - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { disconnect: { id: '1' } } }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: '2' }), - expect.objectContaining({ id: '3' }), - ], - }); - - // not connected - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { disconnect: { id: '1' } } }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: '2' }), - expect.objectContaining({ id: '3' }), - ], - }); - - // non-existing - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - disconnect: [ - { id: '2' }, - { id: '3' }, - { id: '4' }, // non-existing - ], - }, - }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [], - }); - - // multiple - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - disconnect: [{ id: '2' }, { id: '3' }], - }, - }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ comments: [] }); + // not connected + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { disconnect: { id: '1' } } }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: '2' }), expect.objectContaining({ id: '3' })], }); - it('works with nested to-many relation simple delete', async () => { - const user = await createUser(client, 'u1@test.com'); - const post = await client.post.create({ + // non-existing + await expect( + client.post.update({ + where: { id: post.id }, data: { - title: 'Post1', - author: { connect: { id: user.id } }, comments: { - create: [ - { id: '1', content: 'Comment1' }, - { id: '2', content: 'Comment2' }, - { id: '3', content: 'Comment3' }, + disconnect: [ + { id: '2' }, + { id: '3' }, + { id: '4' }, // non-existing ], }, }, - }); - - await client.comment.create({ - data: { id: '4', content: 'Comment4' }, - }); - - // single - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { delete: { id: '1' } } }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: '2' }), - expect.objectContaining({ id: '3' }), - ], - }); - await expect(client.comment.findMany()).toResolveWithLength(3); - - // not connected - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { delete: { id: '4' } } }, - include: { comments: true }, - }) - ).toBeRejectedNotFound(); - await expect(client.comment.findMany()).toResolveWithLength(3); - - // non-existing - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { delete: { id: '5' } } }, - include: { comments: true }, - }) - ).toBeRejectedNotFound(); - await expect(client.comment.findMany()).toResolveWithLength(3); - - // multiple - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - delete: [{ id: '2' }, { id: '3' }], - }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [], + }); + + // multiple + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + disconnect: [{ id: '2' }, { id: '3' }], }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ comments: [] }); - await expect(client.comment.findMany()).toResolveWithLength(1); + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ comments: [] }); + }); + + it('works with nested to-many relation simple delete', async () => { + const user = await createUser(client, 'u1@test.com'); + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + comments: { + create: [ + { id: '1', content: 'Comment1' }, + { id: '2', content: 'Comment2' }, + { id: '3', content: 'Comment3' }, + ], + }, + }, }); - it('works with nested to-many relation deleteMany', async () => { - const user = await createUser(client, 'u1@test.com'); - const post = await client.post.create({ + await client.comment.create({ + data: { id: '4', content: 'Comment4' }, + }); + + // single + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { delete: { id: '1' } } }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: '2' }), expect.objectContaining({ id: '3' })], + }); + await expect(client.comment.findMany()).toResolveWithLength(3); + + // not connected + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { delete: { id: '4' } } }, + include: { comments: true }, + }), + ).toBeRejectedNotFound(); + await expect(client.comment.findMany()).toResolveWithLength(3); + + // non-existing + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { delete: { id: '5' } } }, + include: { comments: true }, + }), + ).toBeRejectedNotFound(); + await expect(client.comment.findMany()).toResolveWithLength(3); + + // multiple + await expect( + client.post.update({ + where: { id: post.id }, data: { - title: 'Post1', - author: { connect: { id: user.id } }, comments: { - create: [ - { id: '1', content: 'Comment1' }, - { id: '2', content: 'Comment2' }, - { id: '3', content: 'Comment3' }, + delete: [{ id: '2' }, { id: '3' }], + }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ comments: [] }); + await expect(client.comment.findMany()).toResolveWithLength(1); + }); + + it('works with nested to-many relation deleteMany', async () => { + const user = await createUser(client, 'u1@test.com'); + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + comments: { + create: [ + { id: '1', content: 'Comment1' }, + { id: '2', content: 'Comment2' }, + { id: '3', content: 'Comment3' }, + ], + }, + }, + }); + + await client.comment.create({ + data: { id: '4', content: 'Comment4' }, + }); + + // none + await expect( + client.post.update({ + where: { id: post.id }, + data: { comments: { deleteMany: [] } }, + }), + ).toResolveTruthy(); + await expect(client.comment.findMany()).toResolveWithLength(4); + + // single + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { deleteMany: { content: 'Comment1' } }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: '2' }), expect.objectContaining({ id: '3' })], + }); + await expect(client.comment.findMany()).toResolveWithLength(3); + + // not connected + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { deleteMany: { content: 'Comment4' } }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: [expect.objectContaining({ id: '2' }), expect.objectContaining({ id: '3' })], + }); + await expect(client.comment.findMany()).toResolveWithLength(3); + + // multiple + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + deleteMany: [ + { content: 'Comment2' }, + { content: 'Comment3' }, + { content: 'Comment5' }, // non-existing ], }, }, - }); - - await client.comment.create({ - data: { id: '4', content: 'Comment4' }, - }); - - // none - await expect( - client.post.update({ - where: { id: post.id }, - data: { comments: { deleteMany: [] } }, - }) - ).toResolveTruthy(); - await expect(client.comment.findMany()).toResolveWithLength(4); - - // single - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { deleteMany: { content: 'Comment1' } }, - }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: '2' }), - expect.objectContaining({ id: '3' }), - ], - }); - await expect(client.comment.findMany()).toResolveWithLength(3); - - // not connected - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { deleteMany: { content: 'Comment4' } }, - }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: [ - expect.objectContaining({ id: '2' }), - expect.objectContaining({ id: '3' }), - ], - }); - await expect(client.comment.findMany()).toResolveWithLength(3); - - // multiple - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - deleteMany: [ - { content: 'Comment2' }, - { content: 'Comment3' }, - { content: 'Comment5' }, // non-existing - ], + include: { comments: true }, + }), + ).resolves.toMatchObject({ comments: [] }); + await expect(client.comment.findMany()).toResolveWithLength(1); + + // all + const post2 = await client.post.create({ + data: { + title: 'Post2', + author: { connect: { id: user.id } }, + comments: { + create: [ + { id: '5', content: 'Comment5' }, + { id: '6', content: 'Comment6' }, + ], + }, + }, + }); + await expect( + client.post.update({ + where: { id: post2.id }, + data: { comments: { deleteMany: {} } }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ comments: [] }); + await expect(client.comment.findMany()).resolves.toEqual([ + expect.objectContaining({ content: 'Comment4' }), + ]); + }); + + it('works with nested to-many relation simple update', async () => { + const user = await createUser(client, 'u1@test.com'); + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + comments: { + create: [ + { id: '1', content: 'Comment1' }, + { id: '2', content: 'Comment2' }, + { id: '3', content: 'Comment3' }, + ], + }, + }, + }); + await client.comment.create({ + data: { id: '4', content: 'Comment4' }, + }); + + // single, toplevel + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + update: { + where: { id: '1' }, + data: { content: 'Comment1-1' }, }, }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ comments: [] }); - await expect(client.comment.findMany()).toResolveWithLength(1); + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: expect.arrayContaining([ + expect.objectContaining({ content: 'Comment1-1' }), + expect.objectContaining({ content: 'Comment2' }), + expect.objectContaining({ content: 'Comment3' }), + ]), + }); - // all - const post2 = await client.post.create({ + // multiple, toplevel + await expect( + client.post.update({ + where: { id: post.id }, data: { - title: 'Post2', - author: { connect: { id: user.id } }, comments: { - create: [ - { id: '5', content: 'Comment5' }, - { id: '6', content: 'Comment6' }, + update: [ + { + where: { id: '2' }, + data: { content: 'Comment2-1' }, + }, + { + where: { id: '3' }, + data: { content: 'Comment3-1' }, + }, ], }, }, - }); - await expect( - client.post.update({ - where: { id: post2.id }, - data: { comments: { deleteMany: {} } }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ comments: [] }); - await expect(client.comment.findMany()).resolves.toEqual([ - expect.objectContaining({ content: 'Comment4' }), - ]); + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: expect.arrayContaining([ + expect.objectContaining({ content: 'Comment1-1' }), + expect.objectContaining({ content: 'Comment2-1' }), + expect.objectContaining({ content: 'Comment3-1' }), + ]), }); - it('works with nested to-many relation simple update', async () => { - const user = await createUser(client, 'u1@test.com'); - const post = await client.post.create({ + // not connected + await expect( + client.post.update({ + where: { id: post.id }, data: { - title: 'Post1', - author: { connect: { id: user.id } }, comments: { - create: [ - { id: '1', content: 'Comment1' }, - { id: '2', content: 'Comment2' }, - { id: '3', content: 'Comment3' }, + update: [ + { + where: { id: '1' }, + data: { content: 'Comment1-2' }, + }, + { + where: { id: '4' }, + data: { content: 'Comment4-1' }, + }, ], }, }, - }); - await client.comment.create({ - data: { id: '4', content: 'Comment4' }, - }); + }), + ).toBeRejectedNotFound(); + // transaction fails as a whole + await expect(client.comment.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ + content: 'Comment1-1', + }); - // single, toplevel - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - update: { + // not found + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + update: [ + { where: { id: '1' }, - data: { content: 'Comment1-1' }, + data: { content: 'Comment1-2' }, }, - }, - }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: expect.arrayContaining([ - expect.objectContaining({ content: 'Comment1-1' }), - expect.objectContaining({ content: 'Comment2' }), - expect.objectContaining({ content: 'Comment3' }), - ]), - }); - - // multiple, toplevel - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - update: [ - { - where: { id: '2' }, - data: { content: 'Comment2-1' }, - }, - { - where: { id: '3' }, - data: { content: 'Comment3-1' }, - }, - ], - }, - }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: expect.arrayContaining([ - expect.objectContaining({ content: 'Comment1-1' }), - expect.objectContaining({ content: 'Comment2-1' }), - expect.objectContaining({ content: 'Comment3-1' }), - ]), - }); - - // not connected - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - update: [ - { - where: { id: '1' }, - data: { content: 'Comment1-2' }, - }, - { - where: { id: '4' }, - data: { content: 'Comment4-1' }, - }, - ], - }, - }, - }) - ).toBeRejectedNotFound(); - // transaction fails as a whole - await expect( - client.comment.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ - content: 'Comment1-1', - }); - - // not found - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - update: [ - { - where: { id: '1' }, - data: { content: 'Comment1-2' }, - }, - { - where: { id: '5' }, - data: { content: 'Comment5-1' }, - }, - ], - }, + { + where: { id: '5' }, + data: { content: 'Comment5-1' }, + }, + ], }, - }) - ).toBeRejectedNotFound(); - // transaction fails as a whole - await expect( - client.comment.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ - content: 'Comment1-1', - }); - - // nested - await expect( - client.user.update({ - where: { id: user.id }, - data: { - posts: { - update: [ - { - where: { id: post.id }, - data: { - comments: { - update: { - where: { id: '1' }, - data: { - content: 'Comment1-2', - }, + }, + }), + ).toBeRejectedNotFound(); + // transaction fails as a whole + await expect(client.comment.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ + content: 'Comment1-1', + }); + + // nested + await expect( + client.user.update({ + where: { id: user.id }, + data: { + posts: { + update: [ + { + where: { id: post.id }, + data: { + comments: { + update: { + where: { id: '1' }, + data: { + content: 'Comment1-2', }, }, }, }, - ], - }, - }, - }) - ).toResolveTruthy(); - await expect( - client.comment.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ - content: 'Comment1-2', - }); - }); - - it('works with nested to-many relation upsert', async () => { - const user = await createUser(client, 'u1@test.com'); - const post = await client.post.create({ - data: { - title: 'Post1', - author: { connect: { id: user.id } }, - }, - }); - await client.comment.create({ - data: { id: '3', content: 'Comment3' }, - }); - - // create, single - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - upsert: { - where: { id: '1' }, - create: { id: '1', content: 'Comment1' }, - update: { content: 'Comment1-1' }, }, - }, + ], }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: expect.arrayContaining([ - expect.objectContaining({ content: 'Comment1' }), - ]), - }); - - // update, single - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - upsert: { - where: { id: '1' }, - create: { content: 'Comment1' }, - update: { content: 'Comment1-1' }, - }, + }, + }), + ).toResolveTruthy(); + await expect(client.comment.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ + content: 'Comment1-2', + }); + }); + + it('works with nested to-many relation upsert', async () => { + const user = await createUser(client, 'u1@test.com'); + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + }, + }); + await client.comment.create({ + data: { id: '3', content: 'Comment3' }, + }); + + // create, single + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + upsert: { + where: { id: '1' }, + create: { id: '1', content: 'Comment1' }, + update: { content: 'Comment1-1' }, }, }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: expect.arrayContaining([ - expect.objectContaining({ content: 'Comment1-1' }), - ]), - }); - - // update, multiple - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - upsert: [ - { - where: { id: '1' }, - create: { content: 'Comment1' }, - update: { content: 'Comment1-2' }, - }, - { - where: { id: '2' }, - create: { content: 'Comment2' }, - update: { content: 'Comment2-2' }, - }, - ], + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: expect.arrayContaining([expect.objectContaining({ content: 'Comment1' })]), + }); + + // update, single + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + upsert: { + where: { id: '1' }, + create: { content: 'Comment1' }, + update: { content: 'Comment1-1' }, }, }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: expect.arrayContaining([ - expect.objectContaining({ content: 'Comment1-2' }), - expect.objectContaining({ content: 'Comment2' }), - ]), - }); - - // not connected - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - upsert: { - where: { id: '3' }, - create: { id: '3', content: 'Comment3' }, - update: { content: 'Comment3-1' }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: expect.arrayContaining([expect.objectContaining({ content: 'Comment1-1' })]), + }); + + // update, multiple + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + upsert: [ + { + where: { id: '1' }, + create: { content: 'Comment1' }, + update: { content: 'Comment1-2' }, }, - }, - }, - }) - ).rejects.toThrow('constraint'); - // transaction fails as a whole - await expect( - client.comment.findUnique({ where: { id: '3' } }) - ).resolves.toMatchObject({ - content: 'Comment3', - }); - - // not found - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - upsert: [ - { - where: { id: '1' }, - create: { content: 'Comment1' }, - update: { content: 'Comment1-2' }, - }, - { - where: { id: '4' }, - create: { - id: '4', - content: 'Comment4', - }, - update: { content: 'Comment4-1' }, - }, - ], - }, - }, - }) - ).toResolveTruthy(); - await expect( - client.comment.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ - content: 'Comment1-2', - }); - await expect( - client.comment.findUnique({ where: { id: '4' } }) - ).resolves.toMatchObject({ content: 'Comment4' }); - - // nested - await expect( - client.user.update({ - where: { id: user.id }, - data: { - posts: { - upsert: { + { where: { id: '2' }, - create: { - title: 'Post2', - comments: { - create: [ - { - id: '5', - content: 'Comment5', - }, - ], - }, - }, - update: { - title: 'Post2-1', - }, + create: { content: 'Comment2' }, + update: { content: 'Comment2-2' }, }, + ], + }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: expect.arrayContaining([ + expect.objectContaining({ content: 'Comment1-2' }), + expect.objectContaining({ content: 'Comment2' }), + ]), + }); + + // not connected + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + upsert: { + where: { id: '3' }, + create: { id: '3', content: 'Comment3' }, + update: { content: 'Comment3-1' }, }, }, - }) - ).toResolveTruthy(); - await expect( - client.comment.findUnique({ where: { id: '5' } }) - ).resolves.toMatchObject({ - content: 'Comment5', - }); + }, + }), + ).rejects.toThrow('constraint'); + // transaction fails as a whole + await expect(client.comment.findUnique({ where: { id: '3' } })).resolves.toMatchObject({ + content: 'Comment3', }); - it('works with nested to-many relation updateMany', async () => { - const user = await createUser(client, 'u1@test.com'); - const post = await client.post.create({ + // not found + await expect( + client.post.update({ + where: { id: post.id }, data: { - title: 'Post1', - author: { connect: { id: user.id } }, comments: { - create: [ - { id: '1', content: 'Comment1' }, - { id: '2', content: 'Comment2' }, - { id: '3', content: 'Comment3' }, + upsert: [ + { + where: { id: '1' }, + create: { content: 'Comment1' }, + update: { content: 'Comment1-2' }, + }, + { + where: { id: '4' }, + create: { + id: '4', + content: 'Comment4', + }, + update: { content: 'Comment4-1' }, + }, ], }, }, - }); - await client.comment.create({ - data: { id: '4', content: 'Comment4' }, - }); - - // single, toplevel - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - updateMany: { - where: { - OR: [ - { content: 'Comment1' }, - { id: '2' }, + }), + ).toResolveTruthy(); + await expect(client.comment.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ + content: 'Comment1-2', + }); + await expect(client.comment.findUnique({ where: { id: '4' } })).resolves.toMatchObject({ + content: 'Comment4', + }); + + // nested + await expect( + client.user.update({ + where: { id: user.id }, + data: { + posts: { + upsert: { + where: { id: '2' }, + create: { + title: 'Post2', + comments: { + create: [ + { + id: '5', + content: 'Comment5', + }, ], }, - data: { content: 'Comment-up' }, + }, + update: { + title: 'Post2-1', }, }, }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: expect.arrayContaining([ - expect.objectContaining({ - id: '1', - content: 'Comment-up', - }), - expect.objectContaining({ - id: '2', - content: 'Comment-up', - }), - expect.objectContaining({ - id: '3', - content: 'Comment3', - }), - ]), - }); - - // multiple, toplevel - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - updateMany: [ - { - where: { content: 'Comment-up' }, - data: { content: 'Comment-up1' }, - }, - { - where: { id: '3' }, - data: { content: 'Comment-up2' }, - }, - ], + }, + }), + ).toResolveTruthy(); + await expect(client.comment.findUnique({ where: { id: '5' } })).resolves.toMatchObject({ + content: 'Comment5', + }); + }); + + it('works with nested to-many relation updateMany', async () => { + const user = await createUser(client, 'u1@test.com'); + const post = await client.post.create({ + data: { + title: 'Post1', + author: { connect: { id: user.id } }, + comments: { + create: [ + { id: '1', content: 'Comment1' }, + { id: '2', content: 'Comment2' }, + { id: '3', content: 'Comment3' }, + ], + }, + }, + }); + await client.comment.create({ + data: { id: '4', content: 'Comment4' }, + }); + + // single, toplevel + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + updateMany: { + where: { + OR: [{ content: 'Comment1' }, { id: '2' }], + }, + data: { content: 'Comment-up' }, }, }, - include: { comments: true }, - }) - ).resolves.toMatchObject({ - comments: expect.arrayContaining([ - expect.objectContaining({ - id: '1', - content: 'Comment-up1', - }), - expect.objectContaining({ - id: '2', - content: 'Comment-up1', - }), - expect.objectContaining({ - id: '3', - content: 'Comment-up2', - }), - ]), - }); - - // not connected - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - updateMany: { - where: { id: '4' }, - data: { content: 'Comment4-1' }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: expect.arrayContaining([ + expect.objectContaining({ + id: '1', + content: 'Comment-up', + }), + expect.objectContaining({ + id: '2', + content: 'Comment-up', + }), + expect.objectContaining({ + id: '3', + content: 'Comment3', + }), + ]), + }); + + // multiple, toplevel + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + updateMany: [ + { + where: { content: 'Comment-up' }, + data: { content: 'Comment-up1' }, }, + { + where: { id: '3' }, + data: { content: 'Comment-up2' }, + }, + ], + }, + }, + include: { comments: true }, + }), + ).resolves.toMatchObject({ + comments: expect.arrayContaining([ + expect.objectContaining({ + id: '1', + content: 'Comment-up1', + }), + expect.objectContaining({ + id: '2', + content: 'Comment-up1', + }), + expect.objectContaining({ + id: '3', + content: 'Comment-up2', + }), + ]), + }); + + // not connected + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + updateMany: { + where: { id: '4' }, + data: { content: 'Comment4-1' }, }, }, - }) - ).toResolveTruthy(); - // not updated - await expect( - client.comment.findUnique({ where: { id: '4' } }) - ).resolves.toMatchObject({ - content: 'Comment4', - }); - - // not found - await expect( - client.post.update({ - where: { id: post.id }, - data: { - comments: { - updateMany: { - where: { id: '5' }, - data: { content: 'Comment5-1' }, - }, + }, + }), + ).toResolveTruthy(); + // not updated + await expect(client.comment.findUnique({ where: { id: '4' } })).resolves.toMatchObject({ + content: 'Comment4', + }); + + // not found + await expect( + client.post.update({ + where: { id: post.id }, + data: { + comments: { + updateMany: { + where: { id: '5' }, + data: { content: 'Comment5-1' }, }, }, - }) - ).toResolveTruthy(); + }, + }), + ).toResolveTruthy(); + }); + }); + + describe('nested to-one from non-owning side', () => { + it('works with nested to-one relation simple create', async () => { + const user = await createUser(client, 'u1@test.com', {}); + + // create + await expect( + client.user.update({ + where: { id: user.id }, + data: { profile: { create: { id: '1', bio: 'Bio' } } }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ id: '1', bio: 'Bio' }), }); }); - describe('nested to-one from non-owning side', () => { - it('works with nested to-one relation simple create', async () => { - const user = await createUser(client, 'u1@test.com', {}); - - // create - await expect( - client.user.update({ - where: { id: user.id }, - data: { profile: { create: { id: '1', bio: 'Bio' } } }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ id: '1', bio: 'Bio' }), - }); - }); - - it('works with nested to-one relation simple connect', async () => { - const user = await createUser(client, 'u1@test.com', {}); - const profile1 = await client.profile.create({ - data: { id: '1', bio: 'Bio' }, - }); - - // connect without a current connection - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - connect: { id: profile1.id }, - }, + it('works with nested to-one relation simple connect', async () => { + const user = await createUser(client, 'u1@test.com', {}); + const profile1 = await client.profile.create({ + data: { id: '1', bio: 'Bio' }, + }); + + // connect without a current connection + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + connect: { id: profile1.id }, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ id: '1', bio: 'Bio' }), - }); - - // connect with a current connection - const profile2 = await client.profile.create({ - data: { id: '2', bio: 'Bio2' }, - }); - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - connect: { id: profile2.id }, + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ id: '1', bio: 'Bio' }), + }); + + // connect with a current connection + const profile2 = await client.profile.create({ + data: { id: '2', bio: 'Bio2' }, + }); + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + connect: { id: profile2.id }, + }, + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ id: '2', bio: 'Bio2' }), + }); + // old profile is disconnected + await expect(client.profile.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ userId: null }); + // new profile is connected + await expect(client.profile.findUnique({ where: { id: '2' } })).resolves.toMatchObject({ userId: user.id }); + + // connect to a non-existing entity + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + connect: { id: '3' }, + }, + }, + include: { profile: true }, + }), + ).toBeRejectedNotFound(); + }); + + it('works with nested to-one relation connectOrCreate', async () => { + const user = await createUser(client, 'u1@test.com', {}); + + // create + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + connectOrCreate: { + where: { id: '1' }, + create: { id: '1', bio: 'Bio' }, }, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ id: '2', bio: 'Bio2' }), - }); - // old profile is disconnected - await expect( - client.profile.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ userId: null }); - // new profile is connected - await expect( - client.profile.findUnique({ where: { id: '2' } }) - ).resolves.toMatchObject({ userId: user.id }); - - // connect to a non-existing entity - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - connect: { id: '3' }, + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ id: '1', bio: 'Bio' }), + }); + + // connect + const profile2 = await client.profile.create({ + data: { id: '2', bio: 'Bio2' }, + }); + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + connectOrCreate: { + where: { id: profile2.id }, + create: { id: '3', bio: 'Bio3' }, }, }, - include: { profile: true }, - }) - ).toBeRejectedNotFound(); + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ id: '2', bio: 'Bio2' }), }); + // old profile is disconnected + await expect(client.profile.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ userId: null }); + // new profile is connected + await expect(client.profile.findUnique({ where: { id: '2' } })).resolves.toMatchObject({ userId: user.id }); + }); - it('works with nested to-one relation connectOrCreate', async () => { - const user = await createUser(client, 'u1@test.com', {}); + it('works with nested to-one relation disconnect', async () => { + const user = await createUser(client, 'u1@test.com', { + profile: { create: { id: '1', bio: 'Bio' } }, + }); - // create - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - connectOrCreate: { - where: { id: '1' }, - create: { id: '1', bio: 'Bio' }, - }, - }, + // disconnect false + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + disconnect: false, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ id: '1', bio: 'Bio' }), - }); - - // connect - const profile2 = await client.profile.create({ - data: { id: '2', bio: 'Bio2' }, - }); - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - connectOrCreate: { - where: { id: profile2.id }, - create: { id: '3', bio: 'Bio3' }, - }, - }, + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ id: '1' }), + }); + + // disconnect true + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + disconnect: true, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ id: '2', bio: 'Bio2' }), - }); - // old profile is disconnected - await expect( - client.profile.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ userId: null }); - // new profile is connected - await expect( - client.profile.findUnique({ where: { id: '2' } }) - ).resolves.toMatchObject({ userId: user.id }); - }); - - it('works with nested to-one relation disconnect', async () => { - const user = await createUser(client, 'u1@test.com', { - profile: { create: { id: '1', bio: 'Bio' } }, - }); - - // disconnect false - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - disconnect: false, - }, + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: null, + }); + + // disconnect with filter + await client.user.update({ + where: { id: user.id }, + data: { + profile: { connect: { id: '1' } }, + }, + }); + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + disconnect: { id: '1' }, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ id: '1' }), - }); - - // disconnect true - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - disconnect: true, - }, + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: null, + }); + + await expect(client.profile.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ + userId: null, + }); + + // disconnect non-existing + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + disconnect: { id: '2' }, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: null, - }); + }, + include: { profile: true }, + }), + ).toResolveTruthy(); + }); + + it('works with nested to-one relation update', async () => { + const user = await createUser(client, 'u1@test.com', { + profile: { create: { id: '1', bio: 'Bio' } }, + }); - // disconnect with filter - await client.user.update({ + // without where + await expect( + client.user.update({ where: { id: user.id }, data: { - profile: { connect: { id: '1' } }, - }, - }); - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - disconnect: { id: '1' }, + profile: { + update: { + bio: 'Bio1', }, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: null, - }); - - await expect( - client.profile.findUnique({ where: { id: '1' } }) - ).resolves.toMatchObject({ - userId: null, - }); - - // disconnect non-existing - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - disconnect: { id: '2' }, + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ bio: 'Bio1' }), + }); + + // with where + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + update: { + where: { id: '1' }, + data: { bio: 'Bio2' }, }, }, - include: { profile: true }, - }) - ).toResolveTruthy(); + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ bio: 'Bio2' }), }); - it('works with nested to-one relation update', async () => { - const user = await createUser(client, 'u1@test.com', { - profile: { create: { id: '1', bio: 'Bio' } }, - }); - - // without where - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - update: { - bio: 'Bio1', - }, + // non-existing + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + update: { + where: { id: '2' }, + data: { bio: 'Bio3' }, }, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ bio: 'Bio1' }), - }); - - // with where - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - update: { - where: { id: '1' }, - data: { bio: 'Bio2' }, - }, - }, + }, + }), + ).toBeRejectedNotFound(); + + // not connected + const user2 = await createUser(client, 'u2@example.com', {}); + await expect( + client.user.update({ + where: { id: user2.id }, + data: { + profile: { + update: { bio: 'Bio4' }, }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ bio: 'Bio2' }), - }); - - // non-existing - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - update: { - where: { id: '2' }, - data: { bio: 'Bio3' }, - }, + }, + }), + ).toBeRejectedNotFound(); + }); + + it('works with nested to-one relation upsert', async () => { + const user = await createUser(client, 'u1@test.com', {}); + + // create + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + upsert: { + where: { id: '1' }, + create: { id: '1', bio: 'Bio' }, + update: { bio: 'Bio1' }, }, }, - }) - ).toBeRejectedNotFound(); - - // not connected - const user2 = await createUser(client, 'u2@example.com', {}); - await expect( - client.user.update({ - where: { id: user2.id }, - data: { - profile: { - update: { bio: 'Bio4' }, + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ bio: 'Bio' }), + }); + + // update + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + upsert: { + where: { id: '1' }, + create: { id: '1', bio: 'Bio' }, + update: { bio: 'Bio1' }, }, }, - }) - ).toBeRejectedNotFound(); + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ bio: 'Bio1' }), }); + }); - it('works with nested to-one relation upsert', async () => { - const user = await createUser(client, 'u1@test.com', {}); + it('works with nested to-one relation delete', async () => { + const user = await createUser(client, 'u1@test.com', { + profile: { create: { id: '1', bio: 'Bio' } }, + }); - // create - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - upsert: { - where: { id: '1' }, - create: { id: '1', bio: 'Bio' }, - update: { bio: 'Bio1' }, - }, - }, - }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ bio: 'Bio' }), - }); - - // update - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - upsert: { - where: { id: '1' }, - create: { id: '1', bio: 'Bio' }, - update: { bio: 'Bio1' }, - }, - }, - }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ bio: 'Bio1' }), - }); - }); - - it('works with nested to-one relation delete', async () => { - const user = await createUser(client, 'u1@test.com', { - profile: { create: { id: '1', bio: 'Bio' } }, - }); - - // false - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - delete: false, - }, - }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: expect.objectContaining({ id: '1' }), - }); - await expect( - client.profile.findUnique({ where: { id: '1' } }) - ).toResolveTruthy(); - - // true - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - delete: true, - }, - }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: null, - }); - await expect( - client.profile.findUnique({ where: { id: '1' } }) - ).toResolveNull(); - - // with filter - await client.user.update({ + // false + await expect( + client.user.update({ where: { id: user.id }, data: { profile: { - create: { id: '1', bio: 'Bio' }, + delete: false, }, }, - }); - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - delete: { id: '1' }, - }, - }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - profile: null, - }); - await expect( - client.profile.findUnique({ where: { id: '1' } }) - ).toResolveNull(); - - // null relation - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - delete: true, - }, - }, - }) - ).toBeRejectedNotFound(); - - // not connected - await client.profile.create({ - data: { id: '2', bio: 'Bio2' }, - }); - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - delete: { id: '2' }, - }, - }, - }) - ).toBeRejectedNotFound(); + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: expect.objectContaining({ id: '1' }), + }); + await expect(client.profile.findUnique({ where: { id: '1' } })).toResolveTruthy(); - // non-existing - await client.user.update({ + // true + await expect( + client.user.update({ where: { id: user.id }, data: { profile: { - create: { id: '1', bio: 'Bio' }, + delete: true, }, }, - }); - await expect( - client.user.update({ - where: { id: user.id }, - data: { - profile: { - delete: { id: '3' }, - }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: null, + }); + await expect(client.profile.findUnique({ where: { id: '1' } })).toResolveNull(); + + // with filter + await client.user.update({ + where: { id: user.id }, + data: { + profile: { + create: { id: '1', bio: 'Bio' }, + }, + }, + }); + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + delete: { id: '1' }, }, - }) - ).toBeRejectedNotFound(); + }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + profile: null, }); - }); + await expect(client.profile.findUnique({ where: { id: '1' } })).toResolveNull(); - describe('nested to-one from owning side', () => { - it('works with nested to-one owning relation simple create', async () => { - // const user = await createUser(client, 'u1@test.com', {}); - const profile = await client.profile.create({ - data: { id: '1', bio: 'Bio' }, - }); - - // create - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - create: { - id: '1', - email: 'u1@test.com', - }, - }, + // null relation + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + delete: true, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ - id: '1', - email: 'u1@test.com', - }), - }); - }); - - it('works with nested to-one owning relation simple connect', async () => { - const user = await createUser(client, 'u1@test.com', {}); - const profile = await client.profile.create({ - data: { id: '1', bio: 'Bio' }, - }); - - // connect without a current connection - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - connect: { id: user.id }, - }, + }, + }), + ).toBeRejectedNotFound(); + + // not connected + await client.profile.create({ + data: { id: '2', bio: 'Bio2' }, + }); + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + delete: { id: '2' }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ id: user.id }), - }); - - // connect with a current connection - const user2 = await createUser(client, 'u2@test.com', {}); - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - connect: { id: user2.id }, - }, + }, + }), + ).toBeRejectedNotFound(); + + // non-existing + await client.user.update({ + where: { id: user.id }, + data: { + profile: { + create: { id: '1', bio: 'Bio' }, + }, + }, + }); + await expect( + client.user.update({ + where: { id: user.id }, + data: { + profile: { + delete: { id: '3' }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ id: user2.id }), - }); - - // connect to a non-existing entity - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - connect: { id: '3' }, + }, + }), + ).toBeRejectedNotFound(); + }); + }); + + describe('nested to-one from owning side', () => { + it('works with nested to-one owning relation simple create', async () => { + // const user = await createUser(client, 'u1@test.com', {}); + const profile = await client.profile.create({ + data: { id: '1', bio: 'Bio' }, + }); + + // create + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + create: { + id: '1', + email: 'u1@test.com', }, }, - include: { user: true }, - }) - ).toBeRejectedNotFound(); + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ + id: '1', + email: 'u1@test.com', + }), }); + }); - it('works with nested to-one owning relation connectOrCreate', async () => { - const profile = await client.profile.create({ - data: { id: '1', bio: 'Bio' }, - }); + it('works with nested to-one owning relation simple connect', async () => { + const user = await createUser(client, 'u1@test.com', {}); + const profile = await client.profile.create({ + data: { id: '1', bio: 'Bio' }, + }); - // create - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - connectOrCreate: { - where: { id: '1' }, - create: { id: '1', email: 'u1@test.com' }, - }, - }, + // connect without a current connection + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + connect: { id: user.id }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ id: '1' }), - }); - - // connect - const user2 = await createUser(client, 'u2@test.com', {}); - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - connectOrCreate: { - where: { id: user2.id }, - create: { id: '3', email: 'u3@test.com' }, - }, - }, + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ id: user.id }), + }); + + // connect with a current connection + const user2 = await createUser(client, 'u2@test.com', {}); + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + connect: { id: user2.id }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ id: user2.id }), - }); + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ id: user2.id }), }); - it('works with nested to-one owning relation disconnect', async () => { - const profile = await client.profile.create({ + // connect to a non-existing entity + await expect( + client.profile.update({ + where: { id: profile.id }, data: { - id: '1', - bio: 'Bio', - user: { create: { id: '1', email: 'u1@test.com' } }, - }, - }); - - // false - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - disconnect: false, - }, + user: { + connect: { id: '3' }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ id: '1' }), - }); - - // true - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - disconnect: true, + }, + include: { user: true }, + }), + ).toBeRejectedNotFound(); + }); + + it('works with nested to-one owning relation connectOrCreate', async () => { + const profile = await client.profile.create({ + data: { id: '1', bio: 'Bio' }, + }); + + // create + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + connectOrCreate: { + where: { id: '1' }, + create: { id: '1', email: 'u1@test.com' }, }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: null, - }); + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ id: '1' }), + }); - // filter - await client.profile.update({ + // connect + const user2 = await createUser(client, 'u2@test.com', {}); + await expect( + client.profile.update({ where: { id: profile.id }, data: { - user: { connect: { id: '1' } }, - }, - }); - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - disconnect: { id: '1' }, + user: { + connectOrCreate: { + where: { id: user2.id }, + create: { id: '3', email: 'u3@test.com' }, }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: null, - }); - - // non-existing - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - disconnect: { id: '2' }, - }, + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ id: user2.id }), + }); + }); + + it('works with nested to-one owning relation disconnect', async () => { + const profile = await client.profile.create({ + data: { + id: '1', + bio: 'Bio', + user: { create: { id: '1', email: 'u1@test.com' } }, + }, + }); + + // false + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + disconnect: false, }, - }) - ).toResolveTruthy(); + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ id: '1' }), + }); - // null relation - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - disconnect: true, - }, + // true + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + disconnect: true, }, - }) - ).toResolveTruthy(); + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: null, + }); - // null relation - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - disconnect: { id: '1' }, - }, + // filter + await client.profile.update({ + where: { id: profile.id }, + data: { + user: { connect: { id: '1' } }, + }, + }); + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + disconnect: { id: '1' }, }, - }) - ).toResolveTruthy(); + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: null, }); - it('works with nested to-one owning relation update', async () => { - const profile = await client.profile.create({ + // non-existing + await expect( + client.profile.update({ + where: { id: profile.id }, data: { - id: '1', - bio: 'Bio', - user: { create: { id: '1', email: 'u1@test.com' } }, + user: { + disconnect: { id: '2' }, + }, }, - }); + }), + ).toResolveTruthy(); - // without where - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - update: { - role: 'ADMIN', - }, - }, + // null relation + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + disconnect: true, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ role: 'ADMIN' }), - }); - - // with where - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - update: { - where: { id: '1' }, - data: { role: 'USER' }, - }, - }, + }, + }), + ).toResolveTruthy(); + + // null relation + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + disconnect: { id: '1' }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ role: 'USER' }), - }); - - // non-existing - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - update: { - where: { id: '2' }, - data: { role: 'ADMIN' }, - }, + }, + }), + ).toResolveTruthy(); + }); + + it('works with nested to-one owning relation update', async () => { + const profile = await client.profile.create({ + data: { + id: '1', + bio: 'Bio', + user: { create: { id: '1', email: 'u1@test.com' } }, + }, + }); + + // without where + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + update: { + role: 'ADMIN', }, }, - }) - ).toBeRejectedNotFound(); - - // not connected - const profile2 = await client.profile.create({ - data: { id: '2', bio: 'Bio2' }, - }); - await expect( - client.profile.update({ - where: { id: profile2.id }, - data: { - user: { - update: { role: 'ADMIN' }, + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ role: 'ADMIN' }), + }); + + // with where + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + update: { + where: { id: '1' }, + data: { role: 'USER' }, }, }, - }) - ).toBeRejectedNotFound(); + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ role: 'USER' }), }); - it('works with nested to-one owning relation upsert', async () => { - const profile = await client.profile.create({ - data: { id: '1', bio: 'Bio' }, - }); - - // create - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - upsert: { - where: { id: '1' }, - create: { id: '1', email: 'u1@test.com' }, - update: { email: 'u2@test.com' }, - }, + // non-existing + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + update: { + where: { id: '2' }, + data: { role: 'ADMIN' }, }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ email: 'u1@test.com' }), - }); - - // update - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - upsert: { - where: { id: '1' }, - create: { id: '1', email: 'u1@test.com' }, - update: { email: 'u2@test.com' }, - }, - }, + }, + }), + ).toBeRejectedNotFound(); + + // not connected + const profile2 = await client.profile.create({ + data: { id: '2', bio: 'Bio2' }, + }); + await expect( + client.profile.update({ + where: { id: profile2.id }, + data: { + user: { + update: { role: 'ADMIN' }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ email: 'u2@test.com' }), - }); + }, + }), + ).toBeRejectedNotFound(); + }); + + it('works with nested to-one owning relation upsert', async () => { + const profile = await client.profile.create({ + data: { id: '1', bio: 'Bio' }, }); - it('works with nested to-one owning relation delete', async () => { - let profile = await client.profile.create({ + // create + await expect( + client.profile.update({ + where: { id: profile.id }, data: { - bio: 'Bio', - user: { create: { id: '1', email: 'u1@test.com' } }, + user: { + upsert: { + where: { id: '1' }, + create: { id: '1', email: 'u1@test.com' }, + update: { email: 'u2@test.com' }, + }, + }, }, - }); + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ email: 'u1@test.com' }), + }); - // false - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - delete: false, + // update + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + upsert: { + where: { id: '1' }, + create: { id: '1', email: 'u1@test.com' }, + update: { email: 'u2@test.com' }, }, }, - include: { user: true }, - }) - ).resolves.toMatchObject({ - user: expect.objectContaining({ id: '1' }), - }); - await expect( - client.user.findUnique({ where: { id: '1' } }) - ).toResolveTruthy(); - - // TODO: how to return for cascade delete? - // await expect( - // client.profile.update({ - // where: { id: profile.id }, - // data: { - // user: { - // delete: true, - // }, - // }, - // include: { user: true }, - // }) - // ).toResolveNull(); // cascade delete - // await expect( - // client.user.findUnique({ where: { id: '1' } }) - // ).toResolveNull(); - await client.user.delete({ where: { id: '1' } }); - - // with filter - // profile = await client.profile.create({ - // data: { - // bio: 'Bio', - // user: { create: { id: '1', email: 'u1@test.com' } }, - // }, - // }); - // await expect( - // client.profile.update({ - // where: { id: profile.id }, - // data: { - // user: { - // delete: { id: '1' }, - // }, - // }, - // include: { user: true }, - // }) - // ).toResolveNull(); - // await expect( - // client.user.findUnique({ where: { id: '1' } }) - // ).toResolveNull(); - - // null relation - profile = await client.profile.create({ - data: { - bio: 'Bio', - }, - }); - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - delete: true, - }, + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ email: 'u2@test.com' }), + }); + }); + + it('works with nested to-one owning relation delete', async () => { + let profile = await client.profile.create({ + data: { + bio: 'Bio', + user: { create: { id: '1', email: 'u1@test.com' } }, + }, + }); + + // false + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + delete: false, }, - }) - ).toBeRejectedNotFound(); - - // not connected - await client.user.create({ - data: { id: '2', email: 'u2@test.com' }, - }); - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - delete: { id: '2' }, - }, + }, + include: { user: true }, + }), + ).resolves.toMatchObject({ + user: expect.objectContaining({ id: '1' }), + }); + await expect(client.user.findUnique({ where: { id: '1' } })).toResolveTruthy(); + + // TODO: how to return for cascade delete? + // await expect( + // client.profile.update({ + // where: { id: profile.id }, + // data: { + // user: { + // delete: true, + // }, + // }, + // include: { user: true }, + // }) + // ).toResolveNull(); // cascade delete + // await expect( + // client.user.findUnique({ where: { id: '1' } }) + // ).toResolveNull(); + await client.user.delete({ where: { id: '1' } }); + + // with filter + // profile = await client.profile.create({ + // data: { + // bio: 'Bio', + // user: { create: { id: '1', email: 'u1@test.com' } }, + // }, + // }); + // await expect( + // client.profile.update({ + // where: { id: profile.id }, + // data: { + // user: { + // delete: { id: '1' }, + // }, + // }, + // include: { user: true }, + // }) + // ).toResolveNull(); + // await expect( + // client.user.findUnique({ where: { id: '1' } }) + // ).toResolveNull(); + + // null relation + profile = await client.profile.create({ + data: { + bio: 'Bio', + }, + }); + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + delete: true, }, - }) - ).toBeRejectedNotFound(); + }, + }), + ).toBeRejectedNotFound(); - // non-existing - await client.profile.update({ + // not connected + await client.user.create({ + data: { id: '2', email: 'u2@test.com' }, + }); + await expect( + client.profile.update({ where: { id: profile.id }, data: { user: { - create: { id: '1', email: 'u1@test.com' }, + delete: { id: '2' }, }, }, - }); - await expect( - client.profile.update({ - where: { id: profile.id }, - data: { - user: { - delete: { id: '3' }, - }, - }, - }) - ).toBeRejectedNotFound(); + }), + ).toBeRejectedNotFound(); + + // non-existing + await client.profile.update({ + where: { id: profile.id }, + data: { + user: { + create: { id: '1', email: 'u1@test.com' }, + }, + }, }); + await expect( + client.profile.update({ + where: { id: profile.id }, + data: { + user: { + delete: { id: '3' }, + }, + }, + }), + ).toBeRejectedNotFound(); }); - } -); + }); +}); diff --git a/packages/runtime/test/client-api/upsert.test.ts b/packages/runtime/test/client-api/upsert.test.ts index 212c90eb..34e402f9 100644 --- a/packages/runtime/test/client-api/upsert.test.ts +++ b/packages/runtime/test/client-api/upsert.test.ts @@ -5,72 +5,69 @@ import { createClientSpecs } from './client-specs'; const PG_DB_NAME = 'client-api-upsert-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Client upsert tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Client upsert tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); - }); + beforeEach(async () => { + client = await createClient(); + }); - afterEach(async () => { - await client?.$disconnect(); - }); + afterEach(async () => { + await client?.$disconnect(); + }); - it('works with toplevel upsert', async () => { - // create - await expect( - client.user.upsert({ - where: { id: '1' }, - create: { - id: '1', - email: 'u1@test.com', - name: 'New', - profile: { create: { bio: 'My bio' } }, - }, - update: { name: 'Foo' }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - id: '1', - name: 'New', - profile: { bio: 'My bio' }, - }); + it('works with toplevel upsert', async () => { + // create + await expect( + client.user.upsert({ + where: { id: '1' }, + create: { + id: '1', + email: 'u1@test.com', + name: 'New', + profile: { create: { bio: 'My bio' } }, + }, + update: { name: 'Foo' }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + id: '1', + name: 'New', + profile: { bio: 'My bio' }, + }); - // update - await expect( - client.user.upsert({ - where: { id: '1' }, - create: { - id: '2', - email: 'u2@test.com', - name: 'New', - }, - update: { name: 'Updated' }, - include: { profile: true }, - }) - ).resolves.toMatchObject({ - id: '1', - name: 'Updated', - profile: { bio: 'My bio' }, - }); + // update + await expect( + client.user.upsert({ + where: { id: '1' }, + create: { + id: '2', + email: 'u2@test.com', + name: 'New', + }, + update: { name: 'Updated' }, + include: { profile: true }, + }), + ).resolves.toMatchObject({ + id: '1', + name: 'Updated', + profile: { bio: 'My bio' }, + }); - // id update - await expect( - client.user.upsert({ - where: { id: '1' }, - create: { - id: '2', - email: 'u2@test.com', - name: 'New', - }, - update: { id: '3' }, - }) - ).resolves.toMatchObject({ - id: '3', - name: 'Updated', - }); + // id update + await expect( + client.user.upsert({ + where: { id: '1' }, + create: { + id: '2', + email: 'u2@test.com', + name: 'New', + }, + update: { id: '3' }, + }), + ).resolves.toMatchObject({ + id: '3', + name: 'Updated', }); - } -); + }); +}); diff --git a/packages/runtime/test/client-api/utils.ts b/packages/runtime/test/client-api/utils.ts index 7ee29aec..78646e36 100644 --- a/packages/runtime/test/client-api/utils.ts +++ b/packages/runtime/test/client-api/utils.ts @@ -10,7 +10,7 @@ export async function createUser( name: 'User1', role: 'ADMIN', profile: { create: { bio: 'My bio' } }, - } + }, ) { return client.user.create({ data: { diff --git a/packages/runtime/test/plugin/kysely-on-query.test.ts b/packages/runtime/test/plugin/kysely-on-query.test.ts index 8aec526d..d18f43c0 100644 --- a/packages/runtime/test/plugin/kysely-on-query.test.ts +++ b/packages/runtime/test/plugin/kysely-on-query.test.ts @@ -1,11 +1,5 @@ import SQLite from 'better-sqlite3'; -import { - InsertQueryNode, - Kysely, - PrimitiveValueListNode, - ValuesNode, - type QueryResult, -} from 'kysely'; +import { InsertQueryNode, Kysely, PrimitiveValueListNode, ValuesNode, type QueryResult } from 'kysely'; import { beforeEach, describe, expect, it } from 'vitest'; import { ZenStackClient, type ClientContract } from '../../src/client'; import { schema } from '../test-schema'; @@ -34,7 +28,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { email: 'u1@test.com' }, - }) + }), ).resolves.toMatchObject({ email: 'u1@test.com', }); @@ -53,7 +47,7 @@ describe('Kysely onQuery tests', () => { await expect( _client.user.create({ data: { email: 'u1@test.com' }, - }) + }), ).resolves.toMatchObject({ email: 'u1@test.com', }); @@ -68,20 +62,12 @@ describe('Kysely onQuery tests', () => { return proceed(query); } const valueList = [ - ...( - ((query as InsertQueryNode).values as ValuesNode) - .values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode).values, ]; valueList[0] = 'u2@test.com'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return proceed(newQuery); }, }); @@ -89,7 +75,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { email: 'u1@test.com' }, - }) + }), ).resolves.toMatchObject({ email: 'u2@test.com', }); @@ -115,7 +101,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { id: '1', email: 'u1@test.com' }, - }) + }), ).resolves.toMatchObject({ email: 'u1@test.com', }); @@ -155,7 +141,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { id: '1', email: 'u1@test.com' }, - }) + }), ).rejects.toThrow('constraint failed'); await expect(client.user.findFirst()).toResolveNull(); @@ -175,20 +161,13 @@ describe('Kysely onQuery tests', () => { } called1 = true; const valueList = [ - ...( - ((query as InsertQueryNode).values as ValuesNode) - .values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[1] = 'Marvin2'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return proceed(newQuery); }, }) @@ -200,21 +179,14 @@ describe('Kysely onQuery tests', () => { } called2 = true; const valueList = [ - ...( - ((query as InsertQueryNode).values as ValuesNode) - .values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[0] = 'u2@test.com'; valueList[1] = 'Marvin1'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return proceed(newQuery); }, }); @@ -222,7 +194,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { email: 'u1@test.com', name: 'Marvin' }, - }) + }), ).resolves.toMatchObject({ email: 'u2@test.com', name: 'Marvin2', @@ -257,23 +229,14 @@ describe('Kysely onQuery tests', () => { called2 = true; return transaction(async (txProceed) => { const valueList = [ - ...( - ( - (query as InsertQueryNode) - .values as ValuesNode - ).values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[0] = 'u2@test.com'; valueList[1] = 'Marvin1'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return txProceed(newQuery); }); }, @@ -282,7 +245,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { email: 'u1@test.com', name: 'Marvin' }, - }) + }), ).rejects.toThrow('test error'); await expect(called1).toBe(true); @@ -317,21 +280,14 @@ describe('Kysely onQuery tests', () => { } called2 = true; const valueList = [ - ...( - ((query as InsertQueryNode).values as ValuesNode) - .values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[0] = 'u2@test.com'; valueList[1] = 'Marvin1'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return proceed(newQuery); }, }); @@ -339,7 +295,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { email: 'u1@test.com', name: 'Marvin' }, - }) + }), ).rejects.toThrow('test error'); await expect(called1).toBe(true); @@ -371,21 +327,14 @@ describe('Kysely onQuery tests', () => { } called2 = true; const valueList = [ - ...( - ((query as InsertQueryNode).values as ValuesNode) - .values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[0] = 'u2@test.com'; valueList[1] = 'Marvin1'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return proceed(newQuery); }, }); @@ -394,8 +343,8 @@ describe('Kysely onQuery tests', () => { client.$transaction((tx) => tx.user.create({ data: { email: 'u1@test.com', name: 'Marvin' }, - }) - ) + }), + ), ).rejects.toThrow('test error'); await expect(called1).toBe(true); @@ -417,22 +366,13 @@ describe('Kysely onQuery tests', () => { called1 = true; return transaction(async (txProceed) => { const valueList = [ - ...( - ( - (query as InsertQueryNode) - .values as ValuesNode - ).values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[1] = 'Marvin2'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return txProceed(newQuery); }); }, @@ -446,23 +386,14 @@ describe('Kysely onQuery tests', () => { called2 = true; return transaction(async (txProceed) => { const valueList = [ - ...( - ( - (query as InsertQueryNode) - .values as ValuesNode - ).values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[0] = 'u2@test.com'; valueList[1] = 'Marvin1'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return txProceed(newQuery); }); }, @@ -471,7 +402,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { email: 'u1@test.com', name: 'Marvin' }, - }) + }), ).resolves.toMatchObject({ email: 'u2@test.com', name: 'Marvin2', @@ -494,22 +425,13 @@ describe('Kysely onQuery tests', () => { called1 = true; return transaction(async (txProceed) => { const valueList = [ - ...( - ( - (query as InsertQueryNode) - .values as ValuesNode - ).values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[1] = 'Marvin2'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); const result = await txProceed(newQuery); // create a post for the user @@ -528,23 +450,14 @@ describe('Kysely onQuery tests', () => { called2 = true; return transaction(async (txProceed) => { const valueList = [ - ...( - ( - (query as InsertQueryNode) - .values as ValuesNode - ).values[0] as PrimitiveValueListNode - ).values, + ...(((query as InsertQueryNode).values as ValuesNode).values[0] as PrimitiveValueListNode) + .values, ]; valueList[0] = 'u2@test.com'; valueList[1] = 'Marvin1'; - const newQuery = InsertQueryNode.cloneWith( - query as InsertQueryNode, - { - values: ValuesNode.create([ - PrimitiveValueListNode.create(valueList), - ]), - } - ); + const newQuery = InsertQueryNode.cloneWith(query as InsertQueryNode, { + values: ValuesNode.create([PrimitiveValueListNode.create(valueList)]), + }); return txProceed(newQuery); }); }, @@ -553,7 +466,7 @@ describe('Kysely onQuery tests', () => { await expect( client.user.create({ data: { email: 'u1@test.com', name: 'Marvin' }, - }) + }), ).rejects.toThrow('test error'); await expect(called1).toBe(true); await expect(called2).toBe(true); diff --git a/packages/runtime/test/plugin/mutation-hooks.test.ts b/packages/runtime/test/plugin/mutation-hooks.test.ts index 43877c22..8958afb0 100644 --- a/packages/runtime/test/plugin/mutation-hooks.test.ts +++ b/packages/runtime/test/plugin/mutation-hooks.test.ts @@ -117,10 +117,7 @@ describe('Entity lifecycle tests', () => { if (args.action === 'update' || args.action === 'delete') { expect(args.entities).toEqual([ expect.objectContaining({ - email: - args.action === 'update' - ? 'u1@test.com' - : 'u3@test.com', + email: args.action === 'update' ? 'u1@test.com' : 'u3@test.com', }), ]); } else { @@ -131,10 +128,7 @@ describe('Entity lifecycle tests', () => { if (args.action === 'update' || args.action === 'delete') { expect(args.beforeMutationEntities).toEqual([ expect.objectContaining({ - email: - args.action === 'update' - ? 'u1@test.com' - : 'u3@test.com', + email: args.action === 'update' ? 'u1@test.com' : 'u3@test.com', }), ]); } @@ -177,12 +171,9 @@ describe('Entity lifecycle tests', () => { expect(args.afterMutationEntities).toEqual( expect.arrayContaining([ expect.objectContaining({ - email: - args.action === 'create' - ? 'u1@test.com' - : 'u2@test.com', + email: args.action === 'create' ? 'u1@test.com' : 'u2@test.com', }), - ]) + ]), ); } else { expect(args.afterMutationEntities).toBeUndefined(); @@ -222,7 +213,7 @@ describe('Entity lifecycle tests', () => { expect.arrayContaining([ expect.objectContaining({ email: 'u1@test.com' }), expect.objectContaining({ email: 'u2@test.com' }), - ]) + ]), ); } else if (args.action === 'update') { userUpdateIntercepted = true; @@ -236,7 +227,7 @@ describe('Entity lifecycle tests', () => { email: 'u1@test.com', name: 'A user', }), - ]) + ]), ); } else if (args.action === 'delete') { userDeleteIntercepted = true; @@ -244,7 +235,7 @@ describe('Entity lifecycle tests', () => { expect.arrayContaining([ expect.objectContaining({ email: 'u1@test.com' }), expect.objectContaining({ email: 'u2@test.com' }), - ]) + ]), ); } }, @@ -269,24 +260,17 @@ describe('Entity lifecycle tests', () => { id: 'test', mutationInterceptionFilter: (args) => { return { - intercept: - args.action === 'create' || args.action === 'update', + intercept: args.action === 'create' || args.action === 'update', loadAfterMutationEntity: true, }; }, afterEntityMutation(args) { if (args.action === 'create') { if (args.model === 'Post') { - if ( - (args.afterMutationEntities![0] as any).title === - 'Post1' - ) { + if ((args.afterMutationEntities![0] as any).title === 'Post1') { post1Intercepted = true; } - if ( - (args.afterMutationEntities![0] as any).title === - 'Post2' - ) { + if ((args.afterMutationEntities![0] as any).title === 'Post2') { post2Intercepted = true; } } diff --git a/packages/runtime/test/plugin/query-lifecycle.test.ts b/packages/runtime/test/plugin/query-lifecycle.test.ts index 19858431..3e476187 100644 --- a/packages/runtime/test/plugin/query-lifecycle.test.ts +++ b/packages/runtime/test/plugin/query-lifecycle.test.ts @@ -37,7 +37,7 @@ describe('Query interception tests', () => { await expect( client.user.findFirst({ where: { id: user.id }, - }) + }), ).resolves.toMatchObject(user); expect(hooksCalled).toBe(true); }); @@ -59,7 +59,7 @@ describe('Query interception tests', () => { await expect( client.user.findFirst({ where: { id: user.id }, - }) + }), ).toResolveNull(); expect(hooksCalled).toBe(true); }); @@ -83,7 +83,7 @@ describe('Query interception tests', () => { await expect( client.user.findFirst({ where: { id: user.id }, - }) + }), ).resolves.toMatchObject({ ...user, happy: true, @@ -137,7 +137,7 @@ describe('Query interception tests', () => { await expect( client.user.findFirst({ where: { id: user1.id }, - }) + }), ).resolves.toMatchObject({ ...user2, happy: true, source: 'plugin2' }); expect(hooks1Called).toBe(true); expect(hooks2Called).toBe(true); @@ -166,7 +166,7 @@ describe('Query interception tests', () => { await expect( _client.user.findFirst({ where: { id: '1' }, - }) + }), ).toResolveTruthy(); }); @@ -195,7 +195,7 @@ describe('Query interception tests', () => { await expect( _client.user.findFirst({ where: { id: '1' }, - }) + }), ).toResolveNull(); }); }); diff --git a/packages/runtime/test/policy/auth.test.ts b/packages/runtime/test/policy/auth.test.ts index 4d91d4b7..f00c79e7 100644 --- a/packages/runtime/test/policy/auth.test.ts +++ b/packages/runtime/test/policy/auth.test.ts @@ -16,17 +16,13 @@ model Post { @@allow('read', true) @@allow('create', auth() != null) } -` +`, ); - await expect( - db.post.create({ data: { title: 'abc' } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); const authDb = db.$setAuth({ id: 'user1' }); - await expect( - authDb.post.create({ data: { title: 'abc' } }) - ).toResolveTruthy(); + await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('works with string id id test', async () => { @@ -43,17 +39,13 @@ model Post { @@allow('read', true) @@allow('create', auth().id != null) } - ` + `, ); - await expect( - db.post.create({ data: { title: 'abc' } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); const authDb = db.$setAuth({ id: 'user1' }); - await expect( - authDb.post.create({ data: { title: 'abc' } }) - ).toResolveTruthy(); + await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('works with int id', async () => { @@ -70,17 +62,13 @@ model Post { @@allow('read', true) @@allow('create', auth() != null) } - ` + `, ); - await expect( - db.post.create({ data: { title: 'abc' } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); const authDb = db.$setAuth({ id: 'user1' }); - await expect( - authDb.post.create({ data: { title: 'abc' } }) - ).toResolveTruthy(); + await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('works with field comparison', async () => { @@ -102,26 +90,20 @@ model Post { @@allow('create,read', true) @@allow('update', auth().id == author.id) } - ` + `, ); - await expect( - db.user.create({ data: { id: 'user1' } }) - ).toResolveTruthy(); + await expect(db.user.create({ data: { id: 'user1' } })).toResolveTruthy(); await expect( db.post.create({ data: { id: '1', title: 'abc', authorId: 'user1' }, - }) + }), ).toResolveTruthy(); - await expect( - db.post.update({ where: { id: '1' }, data: { title: 'bcd' } }) - ).toBeRejectedNotFound(); + await expect(db.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedNotFound(); const authDb2 = db.$setAuth({ id: 'user1' }); - await expect( - authDb2.post.update({ where: { id: '1' }, data: { title: 'bcd' } }) - ).toResolveTruthy(); + await expect(authDb2.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); }); it('works with undefined user non-id field', async () => { @@ -144,30 +126,22 @@ model Post { @@allow('create,read', true) @@allow('update', auth().role == 'ADMIN') } - ` + `, ); - await expect( - db.user.create({ data: { id: 'user1', role: 'USER' } }) - ).toResolveTruthy(); + await expect(db.user.create({ data: { id: 'user1', role: 'USER' } })).toResolveTruthy(); await expect( db.post.create({ data: { id: '1', title: 'abc', authorId: 'user1' }, - }) + }), ).toResolveTruthy(); - await expect( - db.post.update({ where: { id: '1' }, data: { title: 'bcd' } }) - ).toBeRejectedNotFound(); + await expect(db.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedNotFound(); const authDb = db.$setAuth({ id: 'user1', role: 'USER' }); - await expect( - authDb.post.update({ where: { id: '1' }, data: { title: 'bcd' } }) - ).toBeRejectedNotFound(); + await expect(authDb.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedNotFound(); const authDb1 = db.$setAuth({ id: 'user2', role: 'ADMIN' }); - await expect( - authDb1.post.update({ where: { id: '1' }, data: { title: 'bcd' } }) - ).toResolveTruthy(); + await expect(authDb1.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); }); it('works with non User auth model', async () => { @@ -187,18 +161,14 @@ model Post { @@allow('read', true) @@allow('create', auth().role == 'ADMIN') } - ` + `, ); const userDb = db.$setAuth({ id: 'user1', role: 'USER' }); - await expect( - userDb.post.create({ data: { title: 'abc' } }) - ).toBeRejectedByPolicy(); + await expect(userDb.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); const adminDb = db.$setAuth({ id: 'user1', role: 'ADMIN' }); - await expect( - adminDb.post.create({ data: { title: 'abc' } }) - ).toResolveTruthy(); + await expect(adminDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); }); it('works with collection predicate', async () => { @@ -231,7 +201,7 @@ model Post { @@allow('all', true) } - ` + `, ); const rawDb = db.$unuseAll(); @@ -243,9 +213,7 @@ model Post { }; // no post - await expect( - db.$setAuth({ id: '1' }).post.create(createPayload) - ).toBeRejectedByPolicy(); + await expect(db.$setAuth({ id: '1' }).post.create(createPayload)).toBeRejectedByPolicy(); // post not published await expect( @@ -254,7 +222,7 @@ model Post { id: '1', posts: [{ id: '1', published: false }], }) - .post.create(createPayload) + .post.create(createPayload), ).toBeRejectedByPolicy(); // no comments @@ -264,7 +232,7 @@ model Post { id: '1', posts: [{ id: '1', published: true }], }) - .post.create(createPayload) + .post.create(createPayload), ).toBeRejectedByPolicy(); // not all comments published @@ -283,7 +251,7 @@ model Post { }, ], }) - .post.create(createPayload) + .post.create(createPayload), ).toBeRejectedByPolicy(); // comments published but parent post is not @@ -300,7 +268,7 @@ model Post { { id: '2', published: true }, ], }) - .post.create(createPayload) + .post.create(createPayload), ).toBeRejectedByPolicy(); await expect( @@ -316,7 +284,7 @@ model Post { { id: '2', published: false }, ], }) - .post.create(createPayload) + .post.create(createPayload), ).toResolveTruthy(); // no comments ("every" evaluates to true in this case) @@ -326,7 +294,7 @@ model Post { id: '1', posts: [{ id: '1', published: true, comments: [] }], }) - .post.create(createPayload) + .post.create(createPayload), ).toResolveTruthy(); }); @@ -348,21 +316,15 @@ model Post { @@allow('all', true) } - ` + `, ); const userDb = db.$setAuth({ id: '1', name: 'user1', score: 10 }); - await expect( - userDb.post.create({ data: { title: 'abc' } }) - ).toResolveTruthy(); + await expect(userDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); await expect(userDb.post.findMany()).resolves.toHaveLength(1); - await expect( - userDb.post.count({ where: { authorName: 'user1', score: 10 } }) - ).resolves.toBe(1); + await expect(userDb.post.count({ where: { authorName: 'user1', score: 10 } })).resolves.toBe(1); - await expect( - userDb.post.createMany({ data: [{ title: 'def' }] }) - ).resolves.toMatchObject({ count: 1 }); + await expect(userDb.post.createMany({ data: [{ title: 'def' }] })).resolves.toMatchObject({ count: 1 }); const r = await userDb.post.createManyAndReturn({ data: [{ title: 'xxx' }, { title: 'yyy' }], }); @@ -370,7 +332,7 @@ model Post { expect.arrayContaining([ expect.objectContaining({ title: 'xxx', score: 10 }), expect.objectContaining({ title: 'yyy', score: 10 }), - ]) + ]), ); }); @@ -389,25 +351,17 @@ model Post { @@allow('all', true) } - ` + `, ); const userContextName = 'user1'; const overrideName = 'no-default-auth-name'; const userDb = db.$setAuth({ id: '1', name: userContextName }); - await expect( - userDb.post.create({ data: { authorName: overrideName } }) - ).toResolveTruthy(); - await expect( - userDb.post.count({ where: { authorName: overrideName } }) - ).resolves.toBe(1); + await expect(userDb.post.create({ data: { authorName: overrideName } })).toResolveTruthy(); + await expect(userDb.post.count({ where: { authorName: overrideName } })).resolves.toBe(1); - await expect( - userDb.post.createMany({ data: [{ authorName: overrideName }] }) - ).toResolveTruthy(); - await expect( - userDb.post.count({ where: { authorName: overrideName } }) - ).resolves.toBe(2); + await expect(userDb.post.createMany({ data: [{ authorName: overrideName }] })).toResolveTruthy(); + await expect(userDb.post.count({ where: { authorName: overrideName } })).resolves.toBe(2); const r = await userDb.post.createManyAndReturn({ data: [{ authorName: overrideName }], @@ -435,7 +389,7 @@ model Post { @@allow('all', true) } - ` + `, ); const rawDb = anonDb.$unuseAll(); @@ -449,9 +403,7 @@ model Post { const db = anonDb.$setAuth({ id: 'userId-1' }); // default auth effective - await expect( - db.post.create({ data: { title: 'post1' } }) - ).resolves.toMatchObject({ authorId: 'userId-1' }); + await expect(db.post.create({ data: { title: 'post1' } })).resolves.toMatchObject({ authorId: 'userId-1' }); // default auth ineffective due to explicit connect await expect( @@ -460,7 +412,7 @@ model Post { title: 'post2', author: { connect: { email: 'user1@abc.com' } }, }, - }) + }), ).resolves.toMatchObject({ authorId: 'userId-1' }); // default auth ineffective due to explicit connect @@ -470,7 +422,7 @@ model Post { title: 'post3', author: { connect: { email: 'user2@abc.com' } }, }, - }) + }), ).resolves.toMatchObject({ authorId: 'userId-2' }); // TODO: upsert @@ -483,9 +435,7 @@ model Post { // ).resolves.toMatchObject({ authorId: 'userId-1' }); // default auth effective for createMany - await expect( - db.post.createMany({ data: { title: 'post5' } }) - ).resolves.toMatchObject({ count: 1 }); + await expect(db.post.createMany({ data: { title: 'post5' } })).resolves.toMatchObject({ count: 1 }); const r = await db.post.findFirst({ where: { title: 'post5' } }); expect(r).toMatchObject({ authorId: 'userId-1' }); @@ -530,7 +480,7 @@ model Post { @@allow('all', true) } - ` + `, ); const url = 'https://zenstack.dev'; const db = anonDb.$setAuth({ @@ -539,17 +489,15 @@ model Post { }); // top-level create - await expect( - db.user.create({ data: { id: 'userId-1' } }) - ).toResolveTruthy(); + await expect(db.user.create({ data: { id: 'userId-1' } })).toResolveTruthy(); await expect( db.post.create({ data: { title: 'abc', author: { connect: { id: 'userId-1' } } }, - }) + }), ).resolves.toMatchObject({ defaultImageUrl: url }); // nested create - let result = await db.user.create({ + const result = await db.user.create({ data: { id: 'userId-2', posts: { @@ -562,7 +510,7 @@ model Post { expect.arrayContaining([ expect.objectContaining({ title: 'p1', defaultImageUrl: url }), expect.objectContaining({ title: 'p2', defaultImageUrl: url }), - ]) + ]), ); }); @@ -584,15 +532,11 @@ model Post { @@allow('all', true) } - ` + `, ); - await expect( - db.user.create({ data: { id: 'userId-1' } }) - ).toResolveTruthy(); - await expect( - db.post.create({ data: { title: 'title' } }) - ).rejects.toThrow('constraint failed'); + await expect(db.user.create({ data: { id: 'userId-1' } })).toResolveTruthy(); + await expect(db.post.create({ data: { title: 'title' } })).rejects.toThrow('constraint failed'); await expect(db.post.findMany({})).toResolveTruthy(); }); @@ -625,7 +569,7 @@ model Post { @@allow('all', true) } - ` + `, ); const db = anonDb.$setAuth({ id: 'userId-1' }); @@ -633,15 +577,13 @@ model Post { // unchecked context await db.stats.create({ data: { id: 'stats-1', viewCount: 10 } }); - await expect( - db.post.create({ data: { title: 'title1', statsId: 'stats-1' } }) - ).toResolveTruthy(); + await expect(db.post.create({ data: { title: 'title1', statsId: 'stats-1' } })).toResolveTruthy(); await db.stats.create({ data: { id: 'stats-2', viewCount: 10 } }); await expect( db.post.createMany({ data: [{ title: 'title2', statsId: 'stats-2' }], - }) + }), ).resolves.toMatchObject({ count: 1, }); @@ -660,7 +602,7 @@ model Post { title: 'title4', stats: { connect: { id: 'stats-4' } }, }, - }) + }), ).toResolveTruthy(); }); }); diff --git a/packages/runtime/test/policy/connect-disconnect.test.ts b/packages/runtime/test/policy/connect-disconnect.test.ts index 5ed4997d..48e45da0 100644 --- a/packages/runtime/test/policy/connect-disconnect.test.ts +++ b/packages/runtime/test/policy/connect-disconnect.test.ts @@ -65,7 +65,7 @@ describe('connect and disconnect tests', () => { }, }, include: { m2: true }, - }) + }), ).resolves.toMatchObject({ m2: [expect.objectContaining({ id: 'm2-1' })], }); @@ -93,7 +93,7 @@ describe('connect and disconnect tests', () => { connect: { id: 'm2-2' }, }, }, - }) + }), ).toBeRejectedNotFound(); // mixed create and connect @@ -116,7 +116,7 @@ describe('connect and disconnect tests', () => { create: { value: 1, deleted: false }, }, }, - }) + }), ).toBeRejectedNotFound(); // connectOrCreate @@ -142,7 +142,7 @@ describe('connect and disconnect tests', () => { }, }, }, - }) + }), ).toBeRejectedNotFound(); }); @@ -161,7 +161,7 @@ describe('connect and disconnect tests', () => { }, }, }, - }) + }), ).toResolveTruthy(); await db.m3.create({ data: { id: 'm3-2', value: 1, deleted: true } }); @@ -175,7 +175,7 @@ describe('connect and disconnect tests', () => { }, }, }, - }) + }), ).toBeRejectedNotFound(); }); @@ -225,7 +225,7 @@ describe('connect and disconnect tests', () => { }, }, include: { m2: true }, - }) + }), ).resolves.toMatchObject({ m2: expect.objectContaining({ id: 'm2-1' }), }); @@ -250,7 +250,7 @@ describe('connect and disconnect tests', () => { connect: { id: 'm2-2' }, }, }, - }) + }), ).toBeRejectedNotFound(); // connectOrCreate @@ -276,7 +276,7 @@ describe('connect and disconnect tests', () => { }, }, }, - }) + }), ).toBeRejectedNotFound(); }); @@ -316,7 +316,7 @@ describe('connect and disconnect tests', () => { db.m1.update({ where: { id: 'm1-2' }, data: { m2: { connect: { id: 'm2-2' } } }, - }) + }), ).toBeRejectedByPolicy(); }); @@ -363,7 +363,7 @@ describe('connect and disconnect tests', () => { m1: { connect: { id: 'm1-1' } }, m2: { connect: { id: 'm2-1' } }, }, - }) + }), ).toResolveTruthy(); await rawDb.m1.create({ data: { id: 'm1-2', value: 1 } }); @@ -376,7 +376,7 @@ describe('connect and disconnect tests', () => { m1: { connect: { id: 'm1-2' } }, m2: { connect: { id: 'm2-2' } }, }, - }) + }), ).toBeRejectedByPolicy(); }); }); diff --git a/packages/runtime/test/policy/create-many-and-return.test.ts b/packages/runtime/test/policy/create-many-and-return.test.ts index b0b110aa..97829ce4 100644 --- a/packages/runtime/test/policy/create-many-and-return.test.ts +++ b/packages/runtime/test/policy/create-many-and-return.test.ts @@ -23,7 +23,7 @@ describe('createManyAndReturn tests', () => { @@allow('read', published) @@allow('create', contains(title, 'hello')) } - ` + `, ); const rawDb = db.$unuseAll(); @@ -38,11 +38,11 @@ describe('createManyAndReturn tests', () => { await expect( db.post.createManyAndReturn({ data: [{ title: 'foo', userId: 1 }], - }) + }), ).toBeRejectedByPolicy(); // success - let r = await db.post.createManyAndReturn({ + const r = await db.post.createManyAndReturn({ data: [{ id: 1, title: 'hello1', userId: 1, published: true }], }); expect(r.length).toBe(1); @@ -54,7 +54,7 @@ describe('createManyAndReturn tests', () => { { id: 2, title: 'hello2', userId: 1, published: true }, { id: 3, title: 'hello3', userId: 1, published: false }, ], - }) + }), ).toResolveWithLength(1); // two are created indeed await expect(rawDb.post.findMany()).resolves.toHaveLength(3); @@ -71,7 +71,7 @@ describe('createManyAndReturn tests', () => { @@allow('all', true) } - ` + `, ); const rawDb = db.$unuseAll(); // create should succeed but one result's title field can't be read back diff --git a/packages/runtime/test/policy/cross-model-field-comparison.test.ts b/packages/runtime/test/policy/cross-model-field-comparison.test.ts index ad495209..53a29ba6 100644 --- a/packages/runtime/test/policy/cross-model-field-comparison.test.ts +++ b/packages/runtime/test/policy/cross-model-field-comparison.test.ts @@ -22,7 +22,7 @@ describe('cross-model field comparison tests', () => { @@allow('all', true) } - ` + `, ); const rawDb = db.$unuseAll(); @@ -40,11 +40,9 @@ describe('cross-model field comparison tests', () => { age: 18, profile: { create: { id: 1, age: 20 } }, }, - }) + }), ).toBeRejectedByPolicy(); - await expect( - rawDb.user.findUnique({ where: { id: 1 } }) - ).toResolveNull(); + await expect(rawDb.user.findUnique({ where: { id: 1 } })).toResolveNull(); await expect( db.user.create({ data: { @@ -52,11 +50,9 @@ describe('cross-model field comparison tests', () => { age: 18, profile: { create: { id: 1, age: 18 } }, }, - }) - ).toResolveTruthy(); - await expect( - rawDb.user.findUnique({ where: { id: 1 } }) + }), ).toResolveTruthy(); + await expect(rawDb.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); await reset(); // createMany @@ -66,28 +62,22 @@ describe('cross-model field comparison tests', () => { await expect( db.user.createMany({ data: [{ id: 1, age: 18, profileId: profile.id }], - }) + }), ).toBeRejectedByPolicy(); - await expect( - rawDb.user.findUnique({ where: { id: 1 } }) - ).toResolveNull(); + await expect(rawDb.user.findUnique({ where: { id: 1 } })).toResolveNull(); await expect( db.user.createMany({ data: { id: 1, age: 20, profileId: profile.id }, - }) - ).toResolveTruthy(); - await expect( - rawDb.user.findUnique({ where: { id: 1 } }) + }), ).toResolveTruthy(); + await expect(rawDb.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); await reset(); // read await rawDb.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } }, }); - await expect( - db.user.findUnique({ where: { id: 1 } }) - ).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); await expect(db.user.findMany()).resolves.toHaveLength(1); await rawDb.user.update({ where: { id: 1 }, data: { age: 20 } }); await expect(db.user.findUnique({ where: { id: 1 } })).toResolveNull(); @@ -99,15 +89,9 @@ describe('cross-model field comparison tests', () => { data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } }, }); // update should succeed but read back is rejected - await expect( - db.user.update({ where: { id: 1 }, data: { age: 20 } }) - ).toBeRejectedByPolicy(); - await expect( - rawDb.user.findUnique({ where: { id: 1 } }) - ).resolves.toMatchObject({ age: 20 }); - await expect( - db.user.update({ where: { id: 1 }, data: { age: 18 } }) - ).toBeRejectedNotFound(); + await expect(db.user.update({ where: { id: 1 }, data: { age: 20 } })).toBeRejectedByPolicy(); + await expect(rawDb.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ age: 20 }); + await expect(db.user.update({ where: { id: 1 }, data: { age: 18 } })).toBeRejectedNotFound(); await reset(); // // post update @@ -175,31 +159,21 @@ describe('cross-model field comparison tests', () => { data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } }, }); // non updatable - await expect( - db.user.updateMany({ data: { age: 18 } }) - ).resolves.toMatchObject({ count: 0 }); + await expect(db.user.updateMany({ data: { age: 18 } })).resolves.toMatchObject({ count: 0 }); await rawDb.user.create({ data: { id: 2, age: 25, profile: { create: { id: 2, age: 25 } } }, }); // one of the two is updatable - await expect( - db.user.updateMany({ data: { age: 30 } }) - ).resolves.toMatchObject({ count: 1 }); - await expect( - rawDb.user.findUnique({ where: { id: 1 } }) - ).resolves.toMatchObject({ age: 18 }); - await expect( - rawDb.user.findUnique({ where: { id: 2 } }) - ).resolves.toMatchObject({ age: 30 }); + await expect(db.user.updateMany({ data: { age: 30 } })).resolves.toMatchObject({ count: 1 }); + await expect(rawDb.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ age: 18 }); + await expect(rawDb.user.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ age: 30 }); await reset(); // delete await rawDb.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } }, }); - await expect( - db.user.delete({ where: { id: 1 } }) - ).toBeRejectedNotFound(); + await expect(db.user.delete({ where: { id: 1 } })).toBeRejectedNotFound(); await expect(rawDb.user.findMany()).resolves.toHaveLength(1); await rawDb.user.update({ where: { id: 1 }, data: { age: 20 } }); await expect(db.user.delete({ where: { id: 1 } })).toResolveTruthy(); diff --git a/packages/runtime/test/policy/current-model.test.ts b/packages/runtime/test/policy/current-model.test.ts index 1ded3078..8ad743b5 100644 --- a/packages/runtime/test/policy/current-model.test.ts +++ b/packages/runtime/test/policy/current-model.test.ts @@ -16,13 +16,11 @@ describe('currentModel tests', () => { @@allow('read', true) @@allow('create', currentModel() == 'User') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with upper case', async () => { @@ -39,13 +37,11 @@ describe('currentModel tests', () => { @@allow('read', true) @@allow('create', currentModel('upper') == 'Post') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with lower case', async () => { @@ -62,13 +58,11 @@ describe('currentModel tests', () => { @@allow('read', true) @@allow('create', currentModel('lower') == 'Post') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with capitalization', async () => { @@ -85,13 +79,11 @@ describe('currentModel tests', () => { @@allow('read', true) @@allow('create', currentModel('capitalize') == 'post') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with uncapitalization', async () => { @@ -108,13 +100,11 @@ describe('currentModel tests', () => { @@allow('read', true) @@allow('create', currentModel('uncapitalize') == 'POST') } - ` + `, ); await expect(db.USER.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.POST.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.POST.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); // TODO: abstract base support @@ -132,13 +122,11 @@ describe('currentModel tests', () => { model Post extends Base { } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); // TODO: delegate support @@ -159,13 +147,11 @@ describe('currentModel tests', () => { model Post extends Base { } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('complains when used outside policies', async () => { @@ -175,11 +161,9 @@ describe('currentModel tests', () => { model User { id String @default(currentModel()) } - ` - ) - ).rejects.toThrow( - 'function "currentModel" is not allowed in the current context: DefaultValue' - ); + `, + ), + ).rejects.toThrow('function "currentModel" is not allowed in the current context: DefaultValue'); }); it('complains when casing argument is invalid', async () => { @@ -190,10 +174,8 @@ describe('currentModel tests', () => { id String @id @@allow('create', currentModel('foo') == 'User') } - ` - ) - ).rejects.toThrow( - 'argument must be one of: "original", "upper", "lower", "capitalize", "uncapitalize"' - ); + `, + ), + ).rejects.toThrow('argument must be one of: "original", "upper", "lower", "capitalize", "uncapitalize"'); }); }); diff --git a/packages/runtime/test/policy/current-operation.test.ts b/packages/runtime/test/policy/current-operation.test.ts index 46309e96..957f8779 100644 --- a/packages/runtime/test/policy/current-operation.test.ts +++ b/packages/runtime/test/policy/current-operation.test.ts @@ -15,13 +15,11 @@ describe('currentOperation tests', () => { @@allow('read', true) @@allow('create', currentOperation() == 'read') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with all rule', async () => { @@ -37,13 +35,11 @@ describe('currentOperation tests', () => { @@allow('read', true) @@allow('create', currentOperation() == 'read') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with upper case', async () => { @@ -59,13 +55,11 @@ describe('currentOperation tests', () => { @@allow('read', true) @@allow('create', currentOperation('upper') == 'READ') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with lower case', async () => { @@ -81,13 +75,11 @@ describe('currentOperation tests', () => { @@allow('read', true) @@allow('create', currentOperation('lower') == 'read') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with capitalization', async () => { @@ -103,13 +95,11 @@ describe('currentOperation tests', () => { @@allow('read', true) @@allow('create', currentOperation('capitalize') == 'create') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('works with uncapitalization', async () => { @@ -125,13 +115,11 @@ describe('currentOperation tests', () => { @@allow('read', true) @@allow('create', currentOperation('uncapitalize') == 'read') } - ` + `, ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect( - db.post.create({ data: { id: 1 } }) - ).toBeRejectedByPolicy(); + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); }); it('complains when used outside policies', async () => { @@ -141,11 +129,9 @@ describe('currentOperation tests', () => { model User { id String @default(currentOperation()) } - ` - ) - ).rejects.toThrow( - 'function "currentOperation" is not allowed in the current context: DefaultValue' - ); + `, + ), + ).rejects.toThrow('function "currentOperation" is not allowed in the current context: DefaultValue'); }); it('complains when casing argument is invalid', async () => { @@ -156,10 +142,8 @@ describe('currentOperation tests', () => { id String @id @@allow('create', currentOperation('foo') == 'User') } - ` - ) - ).rejects.toThrow( - 'argument must be one of: "original", "upper", "lower", "capitalize", "uncapitalize"' - ); + `, + ), + ).rejects.toThrow('argument must be one of: "original", "upper", "lower", "capitalize", "uncapitalize"'); }); }); diff --git a/packages/runtime/test/policy/deep-nested.test.ts b/packages/runtime/test/policy/deep-nested.test.ts index ab5ef628..0be59e24 100644 --- a/packages/runtime/test/policy/deep-nested.test.ts +++ b/packages/runtime/test/policy/deep-nested.test.ts @@ -168,7 +168,7 @@ describe('deep nested operations tests', () => { }, }, }, - }) + }), ).toResolveTruthy(); const r = await db.m1.create({ @@ -218,7 +218,7 @@ describe('deep nested operations tests', () => { }, }, }, - }) + }), ).toBeRejectedByPolicy(); // deep create violation due to deep policy @@ -234,7 +234,7 @@ describe('deep nested operations tests', () => { }, }, }, - }) + }), ).toBeRejectedByPolicy(); // deep connect violation via deep policy: @@deny('create', m2.m4?[value == 100]) @@ -256,7 +256,7 @@ describe('deep nested operations tests', () => { }, }, }, - }) + }), ).toBeRejectedByPolicy(); // create read-back filter: M4 @@deny('read', value == 200) @@ -318,7 +318,7 @@ describe('deep nested operations tests', () => { }, }, }, - }) + }), ).toResolveTruthy(); // deep update with connect/disconnect/delete success @@ -361,7 +361,7 @@ describe('deep nested operations tests', () => { }, }, }, - }) + }), ).toResolveTruthy(); // deep update violation @@ -377,7 +377,7 @@ describe('deep nested operations tests', () => { }, }, }, - }) + }), ).toBeRejectedByPolicy(); // deep update violation via deep policy: @@deny('update', m2.m4?[value == 101]) @@ -398,7 +398,7 @@ describe('deep nested operations tests', () => { db.m1.update({ where: { myId: '2' }, data: { value: 1 }, - }) + }), ).toBeRejectedNotFound(); // update read-back filter: M4 @@deny('read', value == 200) @@ -481,7 +481,7 @@ describe('deep nested operations tests', () => { }, }, }, - }) + }), ).rejects.toThrow('constraint failed'); // createMany skip duplicate @@ -527,13 +527,7 @@ describe('deep nested operations tests', () => { }); const allM4 = await db.m4.findMany({ select: { value: true } }); await expect(allM4).toHaveLength(3); - await expect(allM4).toEqual( - expect.arrayContaining([ - { value: 21 }, - { value: 21 }, - { value: 22 }, - ]) - ); + await expect(allM4).toEqual(expect.arrayContaining([{ value: 21 }, { value: 21 }, { value: 22 }])); // updateMany, filtered out by policy await db.m1.update({ @@ -555,12 +549,8 @@ describe('deep nested operations tests', () => { }, }, }); - await expect( - db.m4.findUnique({ where: { id: 'm4-1' } }) - ).resolves.toMatchObject({ value: 21 }); - await expect( - db.m4.findUnique({ where: { id: 'm4-2' } }) - ).resolves.toMatchObject({ value: 22 }); + await expect(db.m4.findUnique({ where: { id: 'm4-1' } })).resolves.toMatchObject({ value: 21 }); + await expect(db.m4.findUnique({ where: { id: 'm4-2' } })).resolves.toMatchObject({ value: 22 }); // updateMany, success await db.m1.update({ @@ -582,12 +572,8 @@ describe('deep nested operations tests', () => { }, }, }); - await expect( - db.m4.findUnique({ where: { id: 'm4-1' } }) - ).resolves.toMatchObject({ value: 21 }); - await expect( - db.m4.findUnique({ where: { id: 'm4-2' } }) - ).resolves.toMatchObject({ value: 220 }); + await expect(db.m4.findUnique({ where: { id: 'm4-1' } })).resolves.toMatchObject({ value: 21 }); + await expect(db.m4.findUnique({ where: { id: 'm4-2' } })).resolves.toMatchObject({ value: 220 }); // deleteMany, filtered out by policy await db.m1.update({ diff --git a/packages/runtime/test/policy/empty-policy.test.ts b/packages/runtime/test/policy/empty-policy.test.ts index 2aa55c3f..0199122e 100644 --- a/packages/runtime/test/policy/empty-policy.test.ts +++ b/packages/runtime/test/policy/empty-policy.test.ts @@ -9,7 +9,7 @@ describe('empty policy tests', () => { id String @id @default(uuid()) value Int } - ` + `, ); const rawDb = db.$unuseAll(); @@ -18,47 +18,31 @@ describe('empty policy tests', () => { expect(await db.model.findMany()).toHaveLength(0); expect(await db.model.findUnique({ where: { id: '1' } })).toBeNull(); expect(await db.model.findFirst({ where: { id: '1' } })).toBeNull(); - await expect( - db.model.findUniqueOrThrow({ where: { id: '1' } }) - ).toBeRejectedNotFound(); - await expect( - db.model.findFirstOrThrow({ where: { id: '1' } }) - ).toBeRejectedNotFound(); + await expect(db.model.findUniqueOrThrow({ where: { id: '1' } })).toBeRejectedNotFound(); + await expect(db.model.findFirstOrThrow({ where: { id: '1' } })).toBeRejectedNotFound(); - await expect( - db.model.create({ data: { value: 1 } }) - ).toBeRejectedByPolicy(); - await expect( - db.model.createMany({ data: [{ value: 1 }] }) - ).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { value: 1 } })).toBeRejectedByPolicy(); + await expect(db.model.createMany({ data: [{ value: 1 }] })).toBeRejectedByPolicy(); - await expect( - db.model.update({ where: { id: '1' }, data: { value: 1 } }) - ).toBeRejectedNotFound(); - await expect( - db.model.updateMany({ data: { value: 1 } }) - ).resolves.toMatchObject({ count: 0 }); + await expect(db.model.update({ where: { id: '1' }, data: { value: 1 } })).toBeRejectedNotFound(); + await expect(db.model.updateMany({ data: { value: 1 } })).resolves.toMatchObject({ count: 0 }); await expect( db.model.upsert({ where: { id: '1' }, create: { value: 1 }, update: { value: 1 }, - }) + }), ).toBeRejectedByPolicy(); - await expect( - db.model.delete({ where: { id: '1' } }) - ).toBeRejectedNotFound(); + await expect(db.model.delete({ where: { id: '1' } })).toBeRejectedNotFound(); await expect(db.model.deleteMany()).resolves.toMatchObject({ count: 0, }); - await expect( - db.model.aggregate({ _avg: { value: true } }) - ).resolves.toEqual(expect.objectContaining({ _avg: { value: null } })); - await expect( - db.model.groupBy({ by: ['id'], _avg: { value: true } }) - ).resolves.toHaveLength(0); + await expect(db.model.aggregate({ _avg: { value: true } })).resolves.toEqual( + expect.objectContaining({ _avg: { value: null } }), + ); + await expect(db.model.groupBy({ by: ['id'], _avg: { value: true } })).resolves.toHaveLength(0); await expect(db.model.count()).resolves.toEqual(0); }); @@ -77,7 +61,7 @@ describe('empty policy tests', () => { m1 M1 @relation(fields: [m1Id], references:[id]) m1Id String } - ` + `, ); await expect( @@ -87,7 +71,7 @@ describe('empty policy tests', () => { create: [{}], }, }, - }) + }), ).toBeRejectedByPolicy(); }); @@ -106,7 +90,7 @@ describe('empty policy tests', () => { m1 M1 @relation(fields: [m1Id], references:[id]) m1Id String @unique } - ` + `, ); await expect( @@ -116,7 +100,7 @@ describe('empty policy tests', () => { create: {}, }, }, - }) + }), ).toBeRejectedByPolicy(); }); }); diff --git a/packages/runtime/test/policy/field-comparison.test.ts b/packages/runtime/test/policy/field-comparison.test.ts index 42f755dc..7d597d00 100644 --- a/packages/runtime/test/policy/field-comparison.test.ts +++ b/packages/runtime/test/policy/field-comparison.test.ts @@ -13,15 +13,11 @@ describe('field comparison tests', () => { @@allow('create', x > y) @@allow('read', true) } - ` + `, ); - await expect( - db.model.create({ data: { x: 1, y: 2 } }) - ).toBeRejectedByPolicy(); - await expect( - db.model.create({ data: { x: 2, y: 1 } }) - ).toResolveTruthy(); + await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 2, y: 1 } })).toResolveTruthy(); }); it('works with "in" operator', async () => { @@ -38,16 +34,12 @@ describe('field comparison tests', () => { { provider: 'postgresql', dbName: 'field-comparison-tests-operator', - } + }, ); try { - await expect( - db.model.create({ data: { x: 'a', y: ['b', 'c'] } }) - ).toBeRejectedByPolicy(); - await expect( - db.model.create({ data: { x: 'a', y: ['a', 'c'] } }) - ).toResolveTruthy(); + await expect(db.model.create({ data: { x: 'a', y: ['b', 'c'] } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 'a', y: ['a', 'c'] } })).toResolveTruthy(); } finally { await db.$disconnect(); } @@ -67,16 +59,12 @@ describe('field comparison tests', () => { { provider: 'postgresql', dbName: 'field-comparison-tests-operator-2', - } + }, ); try { - await expect( - db.model.create({ data: { x: 'a', y: ['b', 'c'] } }) - ).toBeRejectedByPolicy(); - await expect( - db.model.create({ data: { x: 'a', y: ['a', 'c'] } }) - ).toResolveTruthy(); + await expect(db.model.create({ data: { x: 'a', y: ['b', 'c'] } })).toBeRejectedByPolicy(); + await expect(db.model.create({ data: { x: 'a', y: ['a', 'c'] } })).toResolveTruthy(); } finally { await db.$disconnect(); } @@ -94,8 +82,8 @@ describe('field comparison tests', () => { @@allow('create', x > y) @@allow('read', true) } - ` - ) + `, + ), ).rejects.toThrow(/invalid operand type/); }); }); diff --git a/packages/runtime/test/policy/policy-functions.test.ts b/packages/runtime/test/policy/policy-functions.test.ts index de49b6d1..d37eff4b 100644 --- a/packages/runtime/test/policy/policy-functions.test.ts +++ b/packages/runtime/test/policy/policy-functions.test.ts @@ -10,15 +10,11 @@ describe('policy functions tests', () => { string String @@allow('all', contains(string, 'a')) } - ` + `, ); - await expect( - db.foo.create({ data: { string: 'bcd' } }) - ).toBeRejectedByPolicy(); - await expect( - db.foo.create({ data: { string: 'bac' } }) - ).toResolveTruthy(); + await expect(db.foo.create({ data: { string: 'bcd' } })).toBeRejectedByPolicy(); + await expect(db.foo.create({ data: { string: 'bac' } })).toResolveTruthy(); }); it('supports contains with case-sensitive non-field', async () => { @@ -33,16 +29,12 @@ describe('policy functions tests', () => { id String @id @default(cuid()) @@allow('all', contains(auth().name, 'a')) } - ` + `, ); await expect(db.foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect( - db.$setAuth({ id: 'user1', name: 'bcd' }).foo.create({ data: {} }) - ).toBeRejectedByPolicy(); - await expect( - db.$setAuth({ id: 'user1', name: 'bac' }).foo.create({ data: {} }) - ).toResolveTruthy(); + await expect(db.$setAuth({ id: 'user1', name: 'bcd' }).foo.create({ data: {} })).toBeRejectedByPolicy(); + await expect(db.$setAuth({ id: 'user1', name: 'bac' }).foo.create({ data: {} })).toResolveTruthy(); }); it('supports contains with auth()', async () => { @@ -58,20 +50,14 @@ describe('policy functions tests', () => { string String @@allow('all', contains(string, auth().name)) } - ` + `, ); // 'abc' contains null - await expect( - anonDb.foo.create({ data: { string: 'abc' } }) - ).toResolveTruthy(); + await expect(anonDb.foo.create({ data: { string: 'abc' } })).toResolveTruthy(); const db = anonDb.$setAuth({ id: '1', name: 'a' }); - await expect( - db.foo.create({ data: { string: 'bcd' } }) - ).toBeRejectedByPolicy(); - await expect( - db.foo.create({ data: { string: 'bac' } }) - ).toResolveTruthy(); + await expect(db.foo.create({ data: { string: 'bcd' } })).toBeRejectedByPolicy(); + await expect(db.foo.create({ data: { string: 'bac' } })).toResolveTruthy(); }); it('supports startsWith with field', async () => { @@ -82,15 +68,11 @@ describe('policy functions tests', () => { string String @@allow('all', startsWith(string, 'a')) } - ` + `, ); - await expect( - db.foo.create({ data: { string: 'bac' } }) - ).toBeRejectedByPolicy(); - await expect( - db.foo.create({ data: { string: 'abc' } }) - ).toResolveTruthy(); + await expect(db.foo.create({ data: { string: 'bac' } })).toBeRejectedByPolicy(); + await expect(db.foo.create({ data: { string: 'abc' } })).toResolveTruthy(); }); it('supports startsWith with non-field', async () => { @@ -105,16 +87,12 @@ describe('policy functions tests', () => { id String @id @default(cuid()) @@allow('all', startsWith(auth().name, 'a')) } - ` + `, ); await expect(anonDb.foo.create({ data: {} })).toBeRejectedByPolicy(); await expect(anonDb.foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect( - anonDb - .$setAuth({ id: 'user1', name: 'abc' }) - .foo.create({ data: {} }) - ).toResolveTruthy(); + await expect(anonDb.$setAuth({ id: 'user1', name: 'abc' }).foo.create({ data: {} })).toResolveTruthy(); }); it('supports endsWith with field', async () => { @@ -125,15 +103,11 @@ describe('policy functions tests', () => { string String @@allow('all', endsWith(string, 'a')) } - ` + `, ); - await expect( - db.foo.create({ data: { string: 'bac' } }) - ).toBeRejectedByPolicy(); - await expect( - db.foo.create({ data: { string: 'bca' } }) - ).toResolveTruthy(); + await expect(db.foo.create({ data: { string: 'bac' } })).toBeRejectedByPolicy(); + await expect(db.foo.create({ data: { string: 'bca' } })).toResolveTruthy(); }); it('supports endsWith with non-field', async () => { @@ -148,20 +122,12 @@ describe('policy functions tests', () => { id String @id @default(cuid()) @@allow('all', endsWith(auth().name, 'a')) } - ` + `, ); await expect(anonDb.foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect( - anonDb - .$setAuth({ id: 'user1', name: 'bac' }) - .foo.create({ data: {} }) - ).toBeRejectedByPolicy(); - await expect( - anonDb - .$setAuth({ id: 'user1', name: 'bca' }) - .foo.create({ data: {} }) - ).toResolveTruthy(); + await expect(anonDb.$setAuth({ id: 'user1', name: 'bac' }).foo.create({ data: {} })).toBeRejectedByPolicy(); + await expect(anonDb.$setAuth({ id: 'user1', name: 'bca' }).foo.create({ data: {} })).toResolveTruthy(); }); it('supports in with field', async () => { @@ -172,15 +138,11 @@ describe('policy functions tests', () => { string String @@allow('all', string in ['a', 'b']) } - ` + `, ); - await expect( - db.foo.create({ data: { string: 'c' } }) - ).toBeRejectedByPolicy(); - await expect( - db.foo.create({ data: { string: 'b' } }) - ).toResolveTruthy(); + await expect(db.foo.create({ data: { string: 'c' } })).toBeRejectedByPolicy(); + await expect(db.foo.create({ data: { string: 'b' } })).toResolveTruthy(); }); it('supports in with non-field', async () => { @@ -195,20 +157,12 @@ describe('policy functions tests', () => { id String @id @default(cuid()) @@allow('all', auth().name in ['abc', 'bcd']) } - ` + `, ); await expect(anonDb.foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect( - anonDb - .$setAuth({ id: 'user1', name: 'abd' }) - .foo.create({ data: {} }) - ).toBeRejectedByPolicy(); - await expect( - anonDb - .$setAuth({ id: 'user1', name: 'abc' }) - .foo.create({ data: {} }) - ).toResolveTruthy(); + await expect(anonDb.$setAuth({ id: 'user1', name: 'abd' }).foo.create({ data: {} })).toBeRejectedByPolicy(); + await expect(anonDb.$setAuth({ id: 'user1', name: 'abc' }).foo.create({ data: {} })).toResolveTruthy(); }); it('supports now', async () => { @@ -220,7 +174,7 @@ describe('policy functions tests', () => { @@allow('create,read', true) @@allow('update', now() >= dt) } - ` + `, ); const now = new Date(); @@ -231,8 +185,6 @@ describe('policy functions tests', () => { console.log(created); // violates `dt <= now()` - await expect( - db.foo.update({ where: { id: '1' }, data: { dt: now } }) - ).toBeRejectedNotFound(); + await expect(db.foo.update({ where: { id: '1' }, data: { dt: now } })).toBeRejectedNotFound(); }); }); diff --git a/packages/runtime/test/policy/read.test.ts b/packages/runtime/test/policy/read.test.ts index 4cd68be9..b6cad7f4 100644 --- a/packages/runtime/test/policy/read.test.ts +++ b/packages/runtime/test/policy/read.test.ts @@ -6,90 +6,80 @@ import { schema } from '../test-schema'; const PG_DB_NAME = 'policy-read-tests'; -describe.each(createClientSpecs(PG_DB_NAME))( - 'Read policy tests', - ({ createClient }) => { - let client: ClientContract; +describe.each(createClientSpecs(PG_DB_NAME))('Read policy tests', ({ createClient }) => { + let client: ClientContract; - beforeEach(async () => { - client = await createClient(); - }); + beforeEach(async () => { + client = await createClient(); + }); - afterEach(async () => { - await client?.$disconnect(); - }); + afterEach(async () => { + await client?.$disconnect(); + }); - it('works with ORM API top-level', async () => { - const user = await client.user.create({ - data: { - email: 'a@b.com', - }, - }); + it('works with ORM API top-level', async () => { + const user = await client.user.create({ + data: { + email: 'a@b.com', + }, + }); - // anonymous auth context by default - const anonClient = client.$use(new PolicyPlugin()); - await expect(anonClient.user.findFirst()).toResolveNull(); + // anonymous auth context by default + const anonClient = client.$use(new PolicyPlugin()); + await expect(anonClient.user.findFirst()).toResolveNull(); - const authClient = anonClient.$setAuth({ - id: user.id, - }); - await expect(authClient.user.findFirst()).resolves.toEqual(user); + const authClient = anonClient.$setAuth({ + id: user.id, }); + await expect(authClient.user.findFirst()).resolves.toEqual(user); + }); - it('works with ORM API nested', async () => { - await client.user.create({ - data: { - id: '1', - email: 'a@b.com', - posts: { - create: { - title: 'Post1', - content: 'My post', - published: false, - }, + it('works with ORM API nested', async () => { + await client.user.create({ + data: { + id: '1', + email: 'a@b.com', + posts: { + create: { + title: 'Post1', + content: 'My post', + published: false, }, }, - }); + }, + }); - const anonClient = client.$use(new PolicyPlugin()); - const otherUserClient = anonClient.$setAuth({ id: '2' }); - const r = await otherUserClient.user.findFirst({ - include: { posts: true }, - }); - expect(r?.posts).toHaveLength(0); + const anonClient = client.$use(new PolicyPlugin()); + const otherUserClient = anonClient.$setAuth({ id: '2' }); + const r = await otherUserClient.user.findFirst({ + include: { posts: true }, + }); + expect(r?.posts).toHaveLength(0); - const authClient = anonClient.$setAuth({ id: '1' }); - const r1 = await authClient.user.findFirst({ - include: { posts: true }, - }); - expect(r1?.posts).toHaveLength(1); + const authClient = anonClient.$setAuth({ id: '1' }); + const r1 = await authClient.user.findFirst({ + include: { posts: true }, }); + expect(r1?.posts).toHaveLength(1); + }); - it('works with query builder API', async () => { - const user = await client.user.create({ - data: { - email: 'a@b.com', - }, - }); + it('works with query builder API', async () => { + const user = await client.user.create({ + data: { + email: 'a@b.com', + }, + }); - const anonClient = client.$use(new PolicyPlugin()); - await expect( - anonClient.$qb.selectFrom('User').selectAll().executeTakeFirst() - ).toResolveFalsy(); + const anonClient = client.$use(new PolicyPlugin()); + await expect(anonClient.$qb.selectFrom('User').selectAll().executeTakeFirst()).toResolveFalsy(); - const authClient = anonClient.$setAuth({ id: user.id }); - const foundUser = await authClient.$qb - .selectFrom('User') - .selectAll() - .executeTakeFirstOrThrow(); + const authClient = anonClient.$setAuth({ id: user.id }); + const foundUser = await authClient.$qb.selectFrom('User').selectAll().executeTakeFirstOrThrow(); - if (typeof foundUser.createdAt === 'string') { - expect(Date.parse(foundUser.createdAt)).toEqual( - user.createdAt.getTime() - ); - } else { - expect(foundUser.createdAt).toEqual(user.createdAt); - } - }); - } -); + if (typeof foundUser.createdAt === 'string') { + expect(Date.parse(foundUser.createdAt)).toEqual(user.createdAt.getTime()); + } else { + expect(foundUser.createdAt).toEqual(user.createdAt); + } + }); +}); diff --git a/packages/runtime/test/policy/todo-sample.test.ts b/packages/runtime/test/policy/todo-sample.test.ts index 922801b7..4246969f 100644 --- a/packages/runtime/test/policy/todo-sample.test.ts +++ b/packages/runtime/test/policy/todo-sample.test.ts @@ -8,9 +8,7 @@ describe('todo sample tests', () => { let schema: SchemaDef; beforeAll(async () => { - const r = await generateTsSchemaFromFile( - path.join(__dirname, '../schemas/todo.zmodel') - ); + const r = await generateTsSchemaFromFile(path.join(__dirname, '../schemas/todo.zmodel')); schema = r.schema; }); @@ -37,17 +35,11 @@ describe('todo sample tests', () => { await expect(anonDb.user.create({ data: user1 })).toBeRejectedByPolicy([ 'result is not allowed to be read back', ]); - await expect( - user1Db.user.findUnique({ where: { id: user1.id } }) - ).toResolveTruthy(); - await expect( - user2Db.user.findUnique({ where: { id: user1.id } }) - ).toResolveNull(); + await expect(user1Db.user.findUnique({ where: { id: user1.id } })).toResolveTruthy(); + await expect(user2Db.user.findUnique({ where: { id: user1.id } })).toResolveNull(); // create user2 - await expect( - anonDb.user.create({ data: user2 }) - ).toBeRejectedByPolicy(); + await expect(anonDb.user.create({ data: user2 })).toBeRejectedByPolicy(); await expect(rawDb.user.count()).resolves.toBe(2); // find with user1 should only get user1 @@ -56,9 +48,7 @@ describe('todo sample tests', () => { expect(r[0]).toEqual(expect.objectContaining(user1)); // get user2 as user1 - await expect( - user1Db.user.findUnique({ where: { id: user2.id } }) - ).toResolveNull(); + await expect(user1Db.user.findUnique({ where: { id: user2.id } })).toResolveNull(); await expect( user1Db.space.create({ @@ -74,7 +64,7 @@ describe('todo sample tests', () => { }, }, }, - }) + }), ).toResolveTruthy(); // user2 can't add himself into space1 by setting himself as admin @@ -86,14 +76,14 @@ describe('todo sample tests', () => { userId: user2.id, role: 'ADMIN', }, - }) + }), ).toBeRejectedByPolicy(); // user1 can add user2 as a member await expect( user1Db.spaceUser.create({ data: { spaceId: 'space1', userId: user2.id, role: 'USER' }, - }) + }), ).toResolveTruthy(); // now both user1 and user2 should be visible @@ -105,7 +95,7 @@ describe('todo sample tests', () => { user2Db.user.update({ where: { id: user1.id }, data: { name: 'hello' }, - }) + }), ).toBeRejectedNotFound(); // update user1 as user1 @@ -113,21 +103,15 @@ describe('todo sample tests', () => { user1Db.user.update({ where: { id: user1.id }, data: { name: 'hello' }, - }) + }), ).toResolveTruthy(); // delete user2 as user1 - await expect( - user1Db.user.delete({ where: { id: user2.id } }) - ).toBeRejectedNotFound(); + await expect(user1Db.user.delete({ where: { id: user2.id } })).toBeRejectedNotFound(); // delete user1 as user1 - await expect( - user1Db.user.delete({ where: { id: user1.id } }) - ).toResolveTruthy(); - await expect( - user1Db.user.findUnique({ where: { id: user1.id } }) - ).toResolveNull(); + await expect(user1Db.user.delete({ where: { id: user1.id } })).toResolveTruthy(); + await expect(user1Db.user.findUnique({ where: { id: user1.id } })).toResolveNull(); }); it('works with todo list CRUD', async () => { @@ -149,7 +133,7 @@ describe('todo sample tests', () => { owner: { connect: { id: user1.id } }, space: { connect: { id: space1.id } }, }, - }) + }), ).toBeRejectedByPolicy(); await expect( @@ -160,32 +144,24 @@ describe('todo sample tests', () => { owner: { connect: { id: user1.id } }, space: { connect: { id: space1.id } }, }, - }) + }), ).toResolveTruthy(); await expect(user1Db.list.findMany()).resolves.toHaveLength(1); await expect(anonDb.list.findMany()).resolves.toHaveLength(0); await expect(emptyUIDDb.list.findMany()).resolves.toHaveLength(0); - await expect( - anonDb.list.findUnique({ where: { id: 'list1' } }) - ).toResolveNull(); + await expect(anonDb.list.findUnique({ where: { id: 'list1' } })).toResolveNull(); // accessible to owner - await expect( - user1Db.list.findUnique({ where: { id: 'list1' } }) - ).resolves.toEqual( - expect.objectContaining({ id: 'list1', title: 'List 1' }) + await expect(user1Db.list.findUnique({ where: { id: 'list1' } })).resolves.toEqual( + expect.objectContaining({ id: 'list1', title: 'List 1' }), ); // accessible to user in the space - await expect( - user2Db.list.findUnique({ where: { id: 'list1' } }) - ).toResolveTruthy(); + await expect(user2Db.list.findUnique({ where: { id: 'list1' } })).toResolveTruthy(); // inaccessible to user not in the space - await expect( - user3Db.list.findUnique({ where: { id: 'list1' } }) - ).toResolveNull(); + await expect(user3Db.list.findUnique({ where: { id: 'list1' } })).toResolveNull(); // make a private list await user1Db.list.create({ @@ -199,14 +175,10 @@ describe('todo sample tests', () => { }); // accessible to owner - await expect( - user1Db.list.findUnique({ where: { id: 'list2' } }) - ).toResolveTruthy(); + await expect(user1Db.list.findUnique({ where: { id: 'list2' } })).toResolveTruthy(); // inaccessible to other user in the space - await expect( - user2Db.list.findUnique({ where: { id: 'list2' } }) - ).toResolveNull(); + await expect(user2Db.list.findUnique({ where: { id: 'list2' } })).toResolveNull(); // create a list which doesn't match credential should fail await expect( @@ -217,7 +189,7 @@ describe('todo sample tests', () => { owner: { connect: { id: user2.id } }, space: { connect: { id: space1.id } }, }, - }) + }), ).toBeRejectedByPolicy(); // create a list which doesn't match credential's space should fail @@ -229,7 +201,7 @@ describe('todo sample tests', () => { owner: { connect: { id: user1.id } }, space: { connect: { id: space2.id } }, }, - }) + }), ).toBeRejectedByPolicy(); // update list @@ -239,10 +211,8 @@ describe('todo sample tests', () => { data: { title: 'List 1 updated', }, - }) - ).resolves.toEqual( - expect.objectContaining({ title: 'List 1 updated' }) - ); + }), + ).resolves.toEqual(expect.objectContaining({ title: 'List 1 updated' })); await expect( user2Db.list.update({ @@ -250,19 +220,13 @@ describe('todo sample tests', () => { data: { title: 'List 1 updated', }, - }) + }), ).toBeRejectedNotFound(); // delete list - await expect( - user2Db.list.delete({ where: { id: 'list1' } }) - ).toBeRejectedNotFound(); - await expect( - user1Db.list.delete({ where: { id: 'list1' } }) - ).toResolveTruthy(); - await expect( - user1Db.list.findUnique({ where: { id: 'list1' } }) - ).toResolveNull(); + await expect(user2Db.list.delete({ where: { id: 'list1' } })).toBeRejectedNotFound(); + await expect(user1Db.list.delete({ where: { id: 'list1' } })).toResolveTruthy(); + await expect(user1Db.list.findUnique({ where: { id: 'list1' } })).toResolveNull(); }); it('works with todo CRUD', async () => { @@ -293,7 +257,7 @@ describe('todo sample tests', () => { connect: { id: 'list1' }, }, }, - }) + }), ).toResolveTruthy(); await expect( @@ -306,7 +270,7 @@ describe('todo sample tests', () => { connect: { id: 'list1' }, }, }, - }) + }), ).toResolveTruthy(); // read @@ -320,7 +284,7 @@ describe('todo sample tests', () => { data: { title: 'Todo 1 updated', }, - }) + }), ).toResolveTruthy(); await expect( user1Db.todo.update({ @@ -328,7 +292,7 @@ describe('todo sample tests', () => { data: { title: 'Todo 2 updated', }, - }) + }), ).toResolveTruthy(); // create a private list @@ -353,7 +317,7 @@ describe('todo sample tests', () => { connect: { id: 'list2' }, }, }, - }) + }), ).toResolveTruthy(); // reject because list2 is private @@ -367,7 +331,7 @@ describe('todo sample tests', () => { connect: { id: 'list2' }, }, }, - }) + }), ).toBeRejectedByPolicy(); // update, only owner can update todo in a private list @@ -377,7 +341,7 @@ describe('todo sample tests', () => { data: { title: 'Todo 3 updated', }, - }) + }), ).toResolveTruthy(); await expect( user2Db.todo.update({ @@ -385,7 +349,7 @@ describe('todo sample tests', () => { data: { title: 'Todo 3 updated', }, - }) + }), ).toBeRejectedNotFound(); }); @@ -458,7 +422,7 @@ describe('todo sample tests', () => { data: { owner: { connect: { id: user2.id } }, }, - }) + }), ).toBeRejectedByPolicy(); // change todo's owner @@ -468,7 +432,7 @@ describe('todo sample tests', () => { data: { owner: { connect: { id: user2.id } }, }, - }) + }), ).toBeRejectedByPolicy(); // nested change todo's owner @@ -485,7 +449,7 @@ describe('todo sample tests', () => { }, }, }, - }) + }), ).toBeRejectedByPolicy(); }); }); diff --git a/packages/runtime/test/policy/utils.ts b/packages/runtime/test/policy/utils.ts index d6b1192b..dc599ce2 100644 --- a/packages/runtime/test/policy/utils.ts +++ b/packages/runtime/test/policy/utils.ts @@ -2,15 +2,12 @@ import { PolicyPlugin } from '../../src/plugins/policy'; import type { SchemaDef } from '../../src/schema'; import { createTestClient, type CreateTestClientOptions } from '../utils'; -export function createPolicyTestClient( - schema: string | SchemaDef, - options?: CreateTestClientOptions -) { +export function createPolicyTestClient(schema: string | SchemaDef, options?: CreateTestClientOptions) { return createTestClient( schema as any, { ...options, plugins: [new PolicyPlugin()], - } as CreateTestClientOptions + } as CreateTestClientOptions, ); } diff --git a/packages/runtime/test/query-builder/query-builder.test.ts b/packages/runtime/test/query-builder/query-builder.test.ts index bc9650fa..5e7754e9 100644 --- a/packages/runtime/test/query-builder/query-builder.test.ts +++ b/packages/runtime/test/query-builder/query-builder.test.ts @@ -27,11 +27,7 @@ describe('Client API tests', () => { }) .execute(); - const u1 = await kysely - .selectFrom('User') - .select('email') - .where('id', '=', uid) - .executeTakeFirst(); + const u1 = await kysely.selectFrom('User').select('email').where('id', '=', uid).executeTakeFirst(); expect(u1).toBeTruthy(); await kysely @@ -52,10 +48,7 @@ describe('Client API tests', () => { .executeTakeFirstOrThrow(); expect(u2).toMatchObject({ title: 'Post1', email: 'a@b.com' }); - const u3 = await kysely - .selectFrom('User') - .selectAll() - .executeTakeFirstOrThrow(); + const u3 = await kysely.selectFrom('User').selectAll().executeTakeFirstOrThrow(); expect(u3).toMatchObject({ email: 'a@b.com', role: 'USER' }); }); }); diff --git a/packages/runtime/test/test-schema.ts b/packages/runtime/test/test-schema.ts index da5e4a58..8102b20b 100644 --- a/packages/runtime/test/test-schema.ts +++ b/packages/runtime/test/test-schema.ts @@ -1,8 +1,4 @@ -import { - ExpressionUtils, - type DataSourceProviderType, - type SchemaDef, -} from '../src/schema'; +import { ExpressionUtils, type DataSourceProviderType, type SchemaDef } from '../src/schema'; export const schema = { provider: { @@ -105,12 +101,9 @@ export const schema = { { name: 'condition', value: ExpressionUtils.binary( - ExpressionUtils.member( - ExpressionUtils.call('auth'), - ['id'] - ), + ExpressionUtils.member(ExpressionUtils.call('auth'), ['id']), '==', - ExpressionUtils.field('id') + ExpressionUtils.field('id'), ), }, ], @@ -125,11 +118,7 @@ export const schema = { }, { name: 'condition', - value: ExpressionUtils.binary( - ExpressionUtils.call('auth'), - '!=', - ExpressionUtils._null() - ), + value: ExpressionUtils.binary(ExpressionUtils.call('auth'), '!=', ExpressionUtils._null()), }, ], }, @@ -198,11 +187,7 @@ export const schema = { }, { name: 'condition', - value: ExpressionUtils.binary( - ExpressionUtils.call('auth'), - '==', - ExpressionUtils._null() - ), + value: ExpressionUtils.binary(ExpressionUtils.call('auth'), '==', ExpressionUtils._null()), }, ], }, @@ -217,12 +202,9 @@ export const schema = { { name: 'condition', value: ExpressionUtils.binary( - ExpressionUtils.member( - ExpressionUtils.call('auth'), - ['id'] - ), + ExpressionUtils.member(ExpressionUtils.call('auth'), ['id']), '==', - ExpressionUtils.field('authorId') + ExpressionUtils.field('authorId'), ), }, ], @@ -327,9 +309,7 @@ export const schema = { plugins: {}, } as const satisfies SchemaDef; -export function getSchema( - type: ProviderType -) { +export function getSchema(type: ProviderType) { return { ...schema, provider: { diff --git a/packages/runtime/test/typing/generate.ts b/packages/runtime/test/typing/generate.ts index 869d9fd3..4e7c9b10 100644 --- a/packages/runtime/test/typing/generate.ts +++ b/packages/runtime/test/typing/generate.ts @@ -11,10 +11,7 @@ async function main() { await generator.generate(zmodelPath, [], tsPath); const content = fs.readFileSync(tsPath, 'utf-8'); - fs.writeFileSync( - tsPath, - content.replace(/@zenstackhq\/runtime/g, '../../dist') - ); + fs.writeFileSync(tsPath, content.replace(/@zenstackhq\/runtime/g, '../../dist')); console.log('TS schema generated at:', tsPath); } diff --git a/packages/runtime/test/typing/schema.ts b/packages/runtime/test/typing/schema.ts index e529ee83..1a6ddc45 100644 --- a/packages/runtime/test/typing/schema.ts +++ b/packages/runtime/test/typing/schema.ts @@ -3,243 +3,307 @@ // This file is automatically generated by ZenStack CLI and should not be manually updated. // ////////////////////////////////////////////////////////////////////////////////////////////// -import { type SchemaDef, type OperandExpression, ExpressionUtils } from "../../dist/schema"; +import { type SchemaDef, type OperandExpression, ExpressionUtils } from '../../dist/schema'; export const schema = { provider: { - type: "sqlite" + type: 'sqlite', }, models: { User: { fields: { id: { - type: "Int", + type: 'Int', id: true, - attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], - default: ExpressionUtils.call("autoincrement") + attributes: [ + { name: '@id' }, + { name: '@default', args: [{ name: 'value', value: ExpressionUtils.call('autoincrement') }] }, + ], + default: ExpressionUtils.call('autoincrement'), }, createdAt: { - type: "DateTime", - attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], - default: ExpressionUtils.call("now") + type: 'DateTime', + attributes: [{ name: '@default', args: [{ name: 'value', value: ExpressionUtils.call('now') }] }], + default: ExpressionUtils.call('now'), }, updatedAt: { - type: "DateTime", + type: 'DateTime', updatedAt: true, - attributes: [{ name: "@updatedAt" }] + attributes: [{ name: '@updatedAt' }], }, name: { - type: "String" + type: 'String', }, email: { - type: "String", + type: 'String', unique: true, - attributes: [{ name: "@unique" }] + attributes: [{ name: '@unique' }], }, posts: { - type: "Post", + type: 'Post', array: true, - relation: { opposite: "author" } + relation: { opposite: 'author' }, }, profile: { - type: "Profile", + type: 'Profile', optional: true, - relation: { opposite: "user" } + relation: { opposite: 'user' }, }, postCount: { - type: "Int", - attributes: [{ name: "@computed" }], - computed: true - } + type: 'Int', + attributes: [{ name: '@computed' }], + computed: true, + }, }, - idFields: ["id"], + idFields: ['id'], uniqueFields: { - id: { type: "Int" }, - email: { type: "String" } + id: { type: 'Int' }, + email: { type: 'String' }, }, computedFields: { postCount(): OperandExpression { - throw new Error("This is a stub for computed field"); - } - } + throw new Error('This is a stub for computed field'); + }, + }, }, Post: { fields: { id: { - type: "Int", + type: 'Int', id: true, - attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], - default: ExpressionUtils.call("autoincrement") + attributes: [ + { name: '@id' }, + { name: '@default', args: [{ name: 'value', value: ExpressionUtils.call('autoincrement') }] }, + ], + default: ExpressionUtils.call('autoincrement'), }, title: { - type: "String" + type: 'String', }, content: { - type: "String" + type: 'String', }, author: { - type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], - relation: { opposite: "posts", fields: ["authorId"], references: ["id"] } + type: 'User', + attributes: [ + { + name: '@relation', + args: [ + { name: 'fields', value: ExpressionUtils.array([ExpressionUtils.field('authorId')]) }, + { name: 'references', value: ExpressionUtils.array([ExpressionUtils.field('id')]) }, + ], + }, + ], + relation: { opposite: 'posts', fields: ['authorId'], references: ['id'] }, }, authorId: { - type: "Int", - foreignKeyFor: [ - "author" - ] + type: 'Int', + foreignKeyFor: ['author'], }, tags: { - type: "Tag", + type: 'Tag', array: true, - relation: { opposite: "posts" } + relation: { opposite: 'posts' }, }, meta: { - type: "Meta", + type: 'Meta', optional: true, - relation: { opposite: "post" } - } + relation: { opposite: 'post' }, + }, }, - idFields: ["id"], + idFields: ['id'], uniqueFields: { - id: { type: "Int" } - } + id: { type: 'Int' }, + }, }, Profile: { fields: { id: { - type: "Int", + type: 'Int', id: true, - attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], - default: ExpressionUtils.call("autoincrement") + attributes: [ + { name: '@id' }, + { name: '@default', args: [{ name: 'value', value: ExpressionUtils.call('autoincrement') }] }, + ], + default: ExpressionUtils.call('autoincrement'), }, age: { - type: "Int" + type: 'Int', }, region: { - type: "Region", + type: 'Region', optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("regionCountry"), ExpressionUtils.field("regionCity")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("country"), ExpressionUtils.field("city")]) }] }], - relation: { opposite: "profiles", fields: ["regionCountry", "regionCity"], references: ["country", "city"] } + attributes: [ + { + name: '@relation', + args: [ + { + name: 'fields', + value: ExpressionUtils.array([ + ExpressionUtils.field('regionCountry'), + ExpressionUtils.field('regionCity'), + ]), + }, + { + name: 'references', + value: ExpressionUtils.array([ + ExpressionUtils.field('country'), + ExpressionUtils.field('city'), + ]), + }, + ], + }, + ], + relation: { + opposite: 'profiles', + fields: ['regionCountry', 'regionCity'], + references: ['country', 'city'], + }, }, regionCountry: { - type: "String", + type: 'String', optional: true, - foreignKeyFor: [ - "region" - ] + foreignKeyFor: ['region'], }, regionCity: { - type: "String", + type: 'String', optional: true, - foreignKeyFor: [ - "region" - ] + foreignKeyFor: ['region'], }, user: { - type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], - relation: { opposite: "profile", fields: ["userId"], references: ["id"] } + type: 'User', + attributes: [ + { + name: '@relation', + args: [ + { name: 'fields', value: ExpressionUtils.array([ExpressionUtils.field('userId')]) }, + { name: 'references', value: ExpressionUtils.array([ExpressionUtils.field('id')]) }, + ], + }, + ], + relation: { opposite: 'profile', fields: ['userId'], references: ['id'] }, }, userId: { - type: "Int", + type: 'Int', unique: true, - attributes: [{ name: "@unique" }], - foreignKeyFor: [ - "user" - ] - } + attributes: [{ name: '@unique' }], + foreignKeyFor: ['user'], + }, }, - idFields: ["id"], + idFields: ['id'], uniqueFields: { - id: { type: "Int" }, - userId: { type: "Int" } - } + id: { type: 'Int' }, + userId: { type: 'Int' }, + }, }, Tag: { fields: { id: { - type: "Int", + type: 'Int', id: true, - attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], - default: ExpressionUtils.call("autoincrement") + attributes: [ + { name: '@id' }, + { name: '@default', args: [{ name: 'value', value: ExpressionUtils.call('autoincrement') }] }, + ], + default: ExpressionUtils.call('autoincrement'), }, name: { - type: "String" + type: 'String', }, posts: { - type: "Post", + type: 'Post', array: true, - relation: { opposite: "tags" } - } + relation: { opposite: 'tags' }, + }, }, - idFields: ["id"], + idFields: ['id'], uniqueFields: { - id: { type: "Int" } - } + id: { type: 'Int' }, + }, }, Region: { fields: { country: { - type: "String", - id: true + type: 'String', + id: true, }, city: { - type: "String", - id: true + type: 'String', + id: true, }, zip: { - type: "String", - optional: true + type: 'String', + optional: true, }, profiles: { - type: "Profile", + type: 'Profile', array: true, - relation: { opposite: "region" } - } + relation: { opposite: 'region' }, + }, }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("country"), ExpressionUtils.field("city")]) }] } + { + name: '@@id', + args: [ + { + name: 'fields', + value: ExpressionUtils.array([ + ExpressionUtils.field('country'), + ExpressionUtils.field('city'), + ]), + }, + ], + }, ], - idFields: ["country", "city"], + idFields: ['country', 'city'], uniqueFields: { - country_city: { country: { type: "String" }, city: { type: "String" } } - } + country_city: { country: { type: 'String' }, city: { type: 'String' } }, + }, }, Meta: { fields: { id: { - type: "Int", + type: 'Int', id: true, - attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], - default: ExpressionUtils.call("autoincrement") + attributes: [ + { name: '@id' }, + { name: '@default', args: [{ name: 'value', value: ExpressionUtils.call('autoincrement') }] }, + ], + default: ExpressionUtils.call('autoincrement'), }, reviewed: { - type: "Boolean" + type: 'Boolean', }, published: { - type: "Boolean" + type: 'Boolean', }, post: { - type: "Post", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("postId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], - relation: { opposite: "meta", fields: ["postId"], references: ["id"] } + type: 'Post', + attributes: [ + { + name: '@relation', + args: [ + { name: 'fields', value: ExpressionUtils.array([ExpressionUtils.field('postId')]) }, + { name: 'references', value: ExpressionUtils.array([ExpressionUtils.field('id')]) }, + ], + }, + ], + relation: { opposite: 'meta', fields: ['postId'], references: ['id'] }, }, postId: { - type: "Int", + type: 'Int', unique: true, - attributes: [{ name: "@unique" }], - foreignKeyFor: [ - "post" - ] - } + attributes: [{ name: '@unique' }], + foreignKeyFor: ['post'], + }, }, - idFields: ["id"], + idFields: ['id'], uniqueFields: { - id: { type: "Int" }, - postId: { type: "Int" } - } - } + id: { type: 'Int' }, + postId: { type: 'Int' }, + }, + }, }, - authType: "User", - plugins: {} + authType: 'User', + plugins: {}, } as const satisfies SchemaDef; export type SchemaType = typeof schema; diff --git a/packages/runtime/test/utils.ts b/packages/runtime/test/utils.ts index 86f1b5ed..1feb435b 100644 --- a/packages/runtime/test/utils.ts +++ b/packages/runtime/test/utils.ts @@ -16,7 +16,7 @@ type PostgresSchema = SchemaDef & { provider: { type: 'postgresql' } }; export async function makeSqliteClient( schema: Schema, - extraOptions?: Partial> + extraOptions?: Partial>, ) { const client = new ZenStackClient(schema, { ...extraOptions, @@ -28,9 +28,7 @@ export async function makeSqliteClient( const TEST_PG_CONFIG = { host: process.env['TEST_PG_HOST'] ?? 'localhost', - port: process.env['TEST_PG_PORT'] - ? parseInt(process.env['TEST_PG_PORT']) - : 5432, + port: process.env['TEST_PG_PORT'] ? parseInt(process.env['TEST_PG_PORT']) : 5432, user: process.env['TEST_PG_USER'] ?? 'postgres', password: process.env['TEST_PG_PASSWORD'] ?? 'postgres', }; @@ -38,7 +36,7 @@ const TEST_PG_CONFIG = { export async function makePostgresClient( schema: Schema, dbName: string, - extraOptions?: Partial> + extraOptions?: Partial>, ) { invariant(dbName, 'dbName is required'); const pgClient = new PGClient(TEST_PG_CONFIG); @@ -59,10 +57,7 @@ export async function makePostgresClient( return client; } -export type CreateTestClientOptions = Omit< - ClientOptions, - 'dialectConfig' -> & { +export type CreateTestClientOptions = Omit, 'dialectConfig'> & { provider?: 'sqlite' | 'postgresql'; dbName?: string; usePrismaPush?: boolean; @@ -70,25 +65,21 @@ export type CreateTestClientOptions = Omit< export async function createTestClient( schema: Schema, - options?: CreateTestClientOptions + options?: CreateTestClientOptions, ): Promise; export async function createTestClient( schema: string, - options?: CreateTestClientOptions + options?: CreateTestClientOptions, ): Promise; export async function createTestClient( schema: Schema | string, - options?: CreateTestClientOptions + options?: CreateTestClientOptions, ): Promise { let workDir: string | undefined; let _schema: Schema; if (typeof schema === 'string') { - const generated = await generateTsSchema( - schema, - options?.provider, - options?.dbName - ); + const generated = await generateTsSchema(schema, options?.provider, options?.dbName); workDir = generated.workDir; _schema = generated.schema as Schema; } else { @@ -109,25 +100,17 @@ export async function createTestClient( } const prismaSchema = new PrismaSchemaGenerator(r.model); const prismaSchemaText = await prismaSchema.generate(); - fs.writeFileSync( - path.resolve(workDir, 'schema.prisma'), - prismaSchemaText - ); - execSync( - 'npx prisma db push --schema ./schema.prisma --skip-generate --force-reset', - { - cwd: workDir!, - stdio: 'inherit', - } - ); + fs.writeFileSync(path.resolve(workDir, 'schema.prisma'), prismaSchemaText); + execSync('npx prisma db push --schema ./schema.prisma --skip-generate --force-reset', { + cwd: workDir!, + stdio: 'inherit', + }); } else { if (options?.provider === 'postgresql') { invariant(options?.dbName, 'dbName is required'); const pgClient = new PGClient(TEST_PG_CONFIG); await pgClient.connect(); - await pgClient.query( - `DROP DATABASE IF EXISTS "${options!.dbName}"` - ); + await pgClient.query(`DROP DATABASE IF EXISTS "${options!.dbName}"`); await pgClient.query(`CREATE DATABASE "${options!.dbName}"`); await pgClient.end(); } @@ -142,11 +125,7 @@ export async function createTestClient( } as unknown as ClientOptions['dialectConfig']; } else { _options.dialectConfig = { - database: new SQLite( - options?.usePrismaPush - ? getDbPath(path.join(workDir!, 'schema.prisma')) - : ':memory:' - ), + database: new SQLite(options?.usePrismaPush ? getDbPath(path.join(workDir!, 'schema.prisma')) : ':memory:'), } as unknown as ClientOptions['dialectConfig']; } @@ -173,9 +152,6 @@ function getDbPath(prismaSchemaPath: string) { } const dbPath = found[2]!; // convert 'file:./dev.db' to './dev.db' - const r = path.join( - path.dirname(prismaSchemaPath), - dbPath.replace(/^file:/, '') - ); + const r = path.join(path.dirname(prismaSchemaPath), dbPath.replace(/^file:/, '')); return r; } diff --git a/packages/runtime/test/vitest-ext.ts b/packages/runtime/test/vitest-ext.ts index 6f423169..096b2429 100644 --- a/packages/runtime/test/vitest-ext.ts +++ b/packages/runtime/test/vitest-ext.ts @@ -3,9 +3,7 @@ import { NotFoundError } from '../src/client/errors'; import { RejectedByPolicyError } from '../src/plugins/policy/errors'; function isPromise(value: any) { - return ( - typeof value.then === 'function' && typeof value.catch === 'function' - ); + return typeof value.then === 'function' && typeof value.catch === 'function'; } function expectError(err: any, errorType: any) { @@ -30,8 +28,7 @@ expect.extend({ const r = await received; return { pass: !!r, - message: () => - `Expected promise to resolve to a truthy value, but got ${r}`, + message: () => `Expected promise to resolve to a truthy value, but got ${r}`, }; }, @@ -42,8 +39,7 @@ expect.extend({ const r = await received; return { pass: !r, - message: () => - `Expected promise to resolve to a falsy value, but got ${r}`, + message: () => `Expected promise to resolve to a falsy value, but got ${r}`, }; }, @@ -54,8 +50,7 @@ expect.extend({ const r = await received; return { pass: r === null, - message: () => - `Expected promise to resolve to a null value, but got ${r}`, + message: () => `Expected promise to resolve to a null value, but got ${r}`, }; }, @@ -63,8 +58,7 @@ expect.extend({ const r = await received; return { pass: Array.isArray(r) && r.length === length, - message: () => - `Expected promise to resolve with an array with length ${length}, but got ${r}`, + message: () => `Expected promise to resolve with an array with length ${length}, but got ${r}`, }; }, @@ -83,10 +77,7 @@ expect.extend({ }; }, - async toBeRejectedByPolicy( - received: Promise, - expectedMessages?: string[] - ) { + async toBeRejectedByPolicy(received: Promise, expectedMessages?: string[]) { if (!isPromise(received)) { return { message: () => 'a promise is expected', pass: false }; } @@ -98,8 +89,7 @@ expect.extend({ for (const m of expectedMessages) { if (!message.includes(m)) { return { - message: () => - `expected message not found in error: ${m}, got message: ${message}`, + message: () => `expected message not found in error: ${m}, got message: ${message}`, pass: false, }; } diff --git a/packages/runtime/test/vitest.d.ts b/packages/runtime/test/vitest.d.ts index b547127c..2606a85a 100644 --- a/packages/runtime/test/vitest.d.ts +++ b/packages/runtime/test/vitest.d.ts @@ -1,3 +1,5 @@ + + import 'vitest'; interface CustomMatchers { diff --git a/packages/runtime/vitest.config.ts b/packages/runtime/vitest.config.ts index a556e1c3..c9f6cc7c 100644 --- a/packages/runtime/vitest.config.ts +++ b/packages/runtime/vitest.config.ts @@ -8,5 +8,5 @@ export default mergeConfig( test: { setupFiles: [path.resolve(__dirname, './test/vitest-ext.ts')], }, - }) + }), ); diff --git a/packages/sdk/eslint.config.js b/packages/sdk/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/sdk/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d771cdb2..ae249e29 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -48,6 +48,7 @@ "@types/tmp": "^0.2.6", "decimal.js": "^10.4.3", "kysely": "catalog:", - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*" } } diff --git a/packages/sdk/src/model-utils.ts b/packages/sdk/src/model-utils.ts index 5bd4609a..f20de625 100644 --- a/packages/sdk/src/model-utils.ts +++ b/packages/sdk/src/model-utils.ts @@ -38,18 +38,13 @@ export function isIdField(field: DataModelField) { return true; } - if ( - model.fields.some((f) => hasAttribute(f, '@id')) || - modelLevelIds.length > 0 - ) { + if (model.fields.some((f) => hasAttribute(f, '@id')) || modelLevelIds.length > 0) { // the model already has id field, don't check @unique and @@unique return false; } // then, the first field with @unique can be used as id - const firstUniqueField = model.fields.find((f) => - hasAttribute(f, '@unique') - ); + const firstUniqueField = model.fields.find((f) => hasAttribute(f, '@unique')); if (firstUniqueField) { return firstUniqueField.name === field.name; } @@ -64,16 +59,8 @@ export function isIdField(field: DataModelField) { } export function hasAttribute( - decl: - | DataModel - | TypeDef - | DataModelField - | Enum - | EnumField - | FunctionDecl - | Attribute - | AttributeParam, - name: string + decl: DataModel | TypeDef | DataModelField | Enum | EnumField | FunctionDecl | Attribute | AttributeParam, + name: string, ) { return !!getAttribute(decl, name); } @@ -89,31 +76,25 @@ export function getAttribute( | FunctionDecl | Attribute | AttributeParam, - name: string + name: string, ) { - return ( - decl.attributes as (DataModelAttribute | DataModelFieldAttribute)[] - ).find((attr) => attr.decl.$refText === name); + return (decl.attributes as (DataModelAttribute | DataModelFieldAttribute)[]).find( + (attr) => attr.decl.$refText === name, + ); } /** * Gets `@@id` fields declared at the data model level (including search in base models) */ export function getModelIdFields(model: DataModel) { - const modelsToCheck = model.$baseMerged - ? [model] - : [model, ...getRecursiveBases(model)]; + const modelsToCheck = model.$baseMerged ? [model] : [model, ...getRecursiveBases(model)]; for (const modelToCheck of modelsToCheck) { - const idAttr = modelToCheck.attributes.find( - (attr) => attr.decl.$refText === '@@id' - ); + const idAttr = modelToCheck.attributes.find((attr) => attr.decl.$refText === '@@id'); if (!idAttr) { continue; } - const fieldsArg = idAttr.args.find( - (a) => a.$resolvedParam?.name === 'fields' - ); + const fieldsArg = idAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { continue; } @@ -130,20 +111,14 @@ export function getModelIdFields(model: DataModel) { * Gets `@@unique` fields declared at the data model level (including search in base models) */ export function getModelUniqueFields(model: DataModel) { - const modelsToCheck = model.$baseMerged - ? [model] - : [model, ...getRecursiveBases(model)]; + const modelsToCheck = model.$baseMerged ? [model] : [model, ...getRecursiveBases(model)]; for (const modelToCheck of modelsToCheck) { - const uniqueAttr = modelToCheck.attributes.find( - (attr) => attr.decl.$refText === '@@unique' - ); + const uniqueAttr = modelToCheck.attributes.find((attr) => attr.decl.$refText === '@@unique'); if (!uniqueAttr) { continue; } - const fieldsArg = uniqueAttr.args.find( - (a) => a.$resolvedParam?.name === 'fields' - ); + const fieldsArg = uniqueAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { continue; } @@ -159,7 +134,7 @@ export function getModelUniqueFields(model: DataModel) { export function getRecursiveBases( dataModel: DataModel, includeDelegate = true, - seen = new Set() + seen = new Set(), ): DataModel[] { const result: DataModel[] = []; if (seen.has(dataModel)) { @@ -188,12 +163,7 @@ export function isUniqueField(field: DataModelField) { return true; } const modelIds = getAttribute(field.$container, '@@unique'); - if ( - modelIds && - modelIds.args.some( - (arg) => isLiteralExpr(arg.value) && arg.value.value === field.name - ) - ) { + if (modelIds && modelIds.args.some((arg) => isLiteralExpr(arg.value) && arg.value.value === field.name)) { return true; } return false; @@ -201,11 +171,7 @@ export function isUniqueField(field: DataModelField) { export function isFromStdlib(node: AstNode) { const model = getContainingModel(node); - return ( - !!model && - !!model.$document && - model.$document.uri.path.endsWith('stdlib.zmodel') - ); + return !!model && !!model.$document && model.$document.uri.path.endsWith('stdlib.zmodel'); } export function getContainingModel(node: AstNode | undefined): Model | null { @@ -224,14 +190,10 @@ export function resolved(ref: Reference): T { export function getAuthDecl(model: Model) { let found = model.declarations.find( - (d) => - isDataModel(d) && - d.attributes.some((attr) => attr.decl.$refText === '@@auth') + (d) => isDataModel(d) && d.attributes.some((attr) => attr.decl.$refText === '@@auth'), ); if (!found) { - found = model.declarations.find( - (d) => isDataModel(d) && d.name === 'User' - ); + found = model.declarations.find((d) => isDataModel(d) && d.name === 'User'); } return found; } diff --git a/packages/sdk/src/prisma/prisma-builder.ts b/packages/sdk/src/prisma/prisma-builder.ts index 0711a2d6..f0cf5d32 100644 --- a/packages/sdk/src/prisma/prisma-builder.ts +++ b/packages/sdk/src/prisma/prisma-builder.ts @@ -45,40 +45,37 @@ export class PrismaModel { } toString(): string { - return [ - ...this.datasources, - ...this.generators, - ...this.enums, - ...this.models, - ] + return [...this.datasources, ...this.generators, ...this.enums, ...this.models] .map((d) => d.toString()) .join('\n\n'); } } export class DataSource { - constructor(public name: string, public fields: SimpleField[] = []) {} + constructor( + public name: string, + public fields: SimpleField[] = [], + ) {} toString(): string { return ( `datasource ${this.name} {\n` + - this.fields - .map((f) => indentString(`${f.name} = ${f.text}`)) - .join('\n') + + this.fields.map((f) => indentString(`${f.name} = ${f.text}`)).join('\n') + `\n}` ); } } export class Generator { - constructor(public name: string, public fields: SimpleField[]) {} + constructor( + public name: string, + public fields: SimpleField[], + ) {} toString(): string { return ( `generator ${this.name} {\n` + - this.fields - .map((f) => indentString(`${f.name} = ${f.text}`)) - .join('\n') + + this.fields.map((f) => indentString(`${f.name} = ${f.text}`)).join('\n') + `\n}` ); } @@ -100,7 +97,7 @@ export class DeclarationBase { export class ContainerDeclaration extends DeclarationBase { constructor( documentations: string[] = [], - public attributes: (ContainerAttribute | PassThroughAttribute)[] = [] + public attributes: (ContainerAttribute | PassThroughAttribute)[] = [], ) { super(documentations); } @@ -109,7 +106,7 @@ export class ContainerDeclaration extends DeclarationBase { export class FieldDeclaration extends DeclarationBase { constructor( documentations: string[] = [], - public attributes: (FieldAttribute | PassThroughAttribute)[] = [] + public attributes: (FieldAttribute | PassThroughAttribute)[] = [], ) { super(documentations); } @@ -120,7 +117,7 @@ export class Model extends ContainerDeclaration { constructor( public name: string, public isView: boolean, - documentations: string[] = [] + documentations: string[] = [], ) { super(documentations); } @@ -130,7 +127,7 @@ export class Model extends ContainerDeclaration { type: ModelFieldType | string, attributes: (FieldAttribute | PassThroughAttribute)[] = [], documentations: string[] = [], - addToFront = false + addToFront = false, ): ModelField { const field = new ModelField(name, type, attributes, documentations); if (addToFront) { @@ -148,7 +145,7 @@ export class Model extends ContainerDeclaration { } override toString(): string { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any[] = [...this.fields]; if (this.attributes.length > 0) { @@ -183,13 +180,11 @@ export class ModelFieldType { constructor( public type: ScalarTypes | string, public array?: boolean, - public optional?: boolean + public optional?: boolean, ) {} toString(): string { - return `${this.type}${this.array ? '[]' : ''}${ - this.optional ? '?' : '' - }`; + return `${this.type}${this.array ? '[]' : ''}${this.optional ? '?' : ''}`; } } @@ -198,7 +193,7 @@ export class ModelField extends FieldDeclaration { public name: string, public type: ModelFieldType | string, attributes: (FieldAttribute | PassThroughAttribute)[] = [], - documentations: string[] = [] + documentations: string[] = [], ) { super(documentations, attributes); } @@ -213,34 +208,30 @@ export class ModelField extends FieldDeclaration { return ( super.toString() + `${this.name} ${this.type}` + - (this.attributes.length > 0 - ? ' ' + this.attributes.map((a) => a.toString()).join(' ') - : '') + (this.attributes.length > 0 ? ' ' + this.attributes.map((a) => a.toString()).join(' ') : '') ); } } export class FieldAttribute { - constructor(public name: string, public args: AttributeArg[] = []) {} + constructor( + public name: string, + public args: AttributeArg[] = [], + ) {} toString(): string { - return ( - `${this.name}(` + - this.args.map((a) => a.toString()).join(', ') + - `)` - ); + return `${this.name}(` + this.args.map((a) => a.toString()).join(', ') + `)`; } } export class ContainerAttribute { - constructor(public name: string, public args: AttributeArg[] = []) {} + constructor( + public name: string, + public args: AttributeArg[] = [], + ) {} toString(): string { - return ( - `${this.name}(` + - this.args.map((a) => a.toString()).join(', ') + - `)` - ); + return `${this.name}(` + this.args.map((a) => a.toString()).join(', ') + `)`; } } @@ -258,60 +249,39 @@ export class PassThroughAttribute { export class AttributeArg { constructor( public name: string | undefined, - public value: AttributeArgValue + public value: AttributeArgValue, ) {} toString(): string { - return this.name - ? `${this.name}: ${this.value}` - : this.value.toString(); + return this.name ? `${this.name}: ${this.value}` : this.value.toString(); } } export class AttributeArgValue { constructor( - public type: - | 'String' - | 'FieldReference' - | 'Number' - | 'Boolean' - | 'Array' - | 'FunctionCall', - public value: - | string - | number - | boolean - | FieldReference - | FunctionCall - | AttributeArgValue[] + public type: 'String' | 'FieldReference' | 'Number' | 'Boolean' | 'Array' | 'FunctionCall', + public value: string | number | boolean | FieldReference | FunctionCall | AttributeArgValue[], ) { switch (type) { case 'String': - if (typeof value !== 'string') - throw new Error('Value must be string'); + if (typeof value !== 'string') throw new Error('Value must be string'); break; case 'Number': if (typeof value !== 'number' && typeof value !== 'string') throw new Error('Value must be number or string'); break; case 'Boolean': - if (typeof value !== 'boolean') - throw new Error('Value must be boolean'); + if (typeof value !== 'boolean') throw new Error('Value must be boolean'); break; case 'Array': - if (!Array.isArray(value)) - throw new Error('Value must be array'); + if (!Array.isArray(value)) throw new Error('Value must be array'); break; case 'FieldReference': - if ( - typeof value !== 'string' && - !(value instanceof FieldReference) - ) + if (typeof value !== 'string' && !(value instanceof FieldReference)) throw new Error('Value must be string or FieldReference'); break; case 'FunctionCall': - if (!(value instanceof FunctionCall)) - throw new Error('Value must be FunctionCall'); + if (!(value instanceof FunctionCall)) throw new Error('Value must be FunctionCall'); break; } } @@ -330,10 +300,7 @@ export class AttributeArgValue { const fr = this.value as FieldReference; let r = fr.field; if (fr.args.length > 0) { - r += - '(' + - fr.args.map((a) => a.toString()).join(',') + - ')'; + r += '(' + fr.args.map((a) => a.toString()).join(',') + ')'; } return r; } @@ -343,13 +310,7 @@ export class AttributeArgValue { case 'Boolean': return this.value ? 'true' : 'false'; case 'Array': - return ( - '[' + - (this.value as AttributeArgValue[]) - .map((v) => v.toString()) - .join(', ') + - ']' - ); + return '[' + (this.value as AttributeArgValue[]).map((v) => v.toString()).join(', ') + ']'; default: throw new Error(`Unknown attribute value type ${this.type}`); } @@ -357,11 +318,17 @@ export class AttributeArgValue { } export class FieldReference { - constructor(public field: string, public args: FieldReferenceArg[] = []) {} + constructor( + public field: string, + public args: FieldReferenceArg[] = [], + ) {} } export class FieldReferenceArg { - constructor(public name: string, public value: string) {} + constructor( + public name: string, + public value: string, + ) {} toString(): string { return `${this.name}: ${this.value}`; @@ -369,15 +336,13 @@ export class FieldReferenceArg { } export class FunctionCall { - constructor(public func: string, public args: FunctionCallArg[] = []) {} + constructor( + public func: string, + public args: FunctionCallArg[] = [], + ) {} toString(): string { - return ( - `${this.func}` + - '(' + - this.args.map((a) => a.toString()).join(', ') + - ')' - ); + return `${this.func}` + '(' + this.args.map((a) => a.toString()).join(', ') + ')'; } } @@ -392,13 +357,16 @@ export class FunctionCallArg { export class Enum extends ContainerDeclaration { public fields: EnumField[] = []; - constructor(public name: string, documentations: string[] = []) { + constructor( + public name: string, + documentations: string[] = [], + ) { super(documentations); } addField( name: string, attributes: (FieldAttribute | PassThroughAttribute)[] = [], - documentations: string[] = [] + documentations: string[] = [], ): EnumField { const field = new EnumField(name, attributes, documentations); this.fields.push(field); @@ -420,11 +388,7 @@ export class Enum extends ContainerDeclaration { return ( super.toString() + `enum ${this.name} {\n` + - indentString( - [...this.fields, ...this.attributes] - .map((d) => d.toString()) - .join('\n') - ) + + indentString([...this.fields, ...this.attributes].map((d) => d.toString()).join('\n')) + '\n}' ); } @@ -434,7 +398,7 @@ export class EnumField extends DeclarationBase { constructor( public name: string, public attributes: (FieldAttribute | PassThroughAttribute)[] = [], - documentations: string[] = [] + documentations: string[] = [], ) { super(documentations); } @@ -449,9 +413,7 @@ export class EnumField extends DeclarationBase { return ( super.toString() + this.name + - (this.attributes.length > 0 - ? ' ' + this.attributes.map((a) => a.toString()).join(' ') - : '') + (this.attributes.length > 0 ? ' ' + this.attributes.map((a) => a.toString()).join(' ') : '') ); } } diff --git a/packages/sdk/src/prisma/prisma-schema-generator.ts b/packages/sdk/src/prisma/prisma-schema-generator.ts index 253bedc6..b4edb776 100644 --- a/packages/sdk/src/prisma/prisma-schema-generator.ts +++ b/packages/sdk/src/prisma/prisma-schema-generator.ts @@ -121,13 +121,7 @@ export class PrismaSchemaGenerator { return ( item.name + (item.args.length > 0 - ? '(' + - item.args - .map((arg) => - this.configInvocationArgToText(arg) - ) - .join(', ') + - ')' + ? '(' + item.args.map((arg) => this.configInvocationArgToText(arg)).join(', ') + ')' : '') ); } @@ -151,14 +145,12 @@ export class PrismaSchemaGenerator { decl.fields.map((f) => ({ name: f.name, text: this.configExprToText(f.value), - })) + })), ); } private generateModel(prisma: PrismaModel, decl: DataModel) { - const model = decl.isView - ? prisma.addView(decl.name) - : prisma.addModel(decl.name); + const model = decl.isView ? prisma.addView(decl.name) : prisma.addModel(decl.name); for (const field of decl.fields) { if (ModelUtils.hasAttribute(field, '@computed')) { continue; // skip computed fields @@ -167,9 +159,7 @@ export class PrismaSchemaGenerator { this.generateModelField(model, field); } - for (const attr of decl.attributes.filter((attr) => - this.isPrismaAttribute(attr) - )) { + for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) { this.generateContainerAttribute(model, attr); } @@ -193,15 +183,11 @@ export class PrismaSchemaGenerator { // this.ensureRelationsInheritedFromDelegate(model, decl); } - private isPrismaAttribute( - attr: DataModelAttribute | DataModelFieldAttribute - ) { + private isPrismaAttribute(attr: DataModelAttribute | DataModelFieldAttribute) { if (!attr.decl.ref) { return false; } - return attr.decl.ref.attributes.some( - (a) => a.decl.ref?.name === '@@@prisma' - ); + return attr.decl.ref.attributes.some((a) => a.decl.ref?.name === '@@@prisma'); } private getUnsupportedFieldType(fieldType: DataModelFieldType) { @@ -221,11 +207,7 @@ export class PrismaSchemaGenerator { return isStringLiteral(node) ? node.value : undefined; } - private generateModelField( - model: PrismaDataModel, - field: DataModelField, - addToFront = false - ) { + private generateModelField(model: PrismaDataModel, field: DataModelField, addToFront = false) { let fieldType: string | undefined; if (field.type.type) { @@ -247,19 +229,13 @@ export class PrismaSchemaGenerator { } if (!fieldType) { - throw new Error( - `Field type is not resolved: ${field.$container.name}.${field.name}` - ); + throw new Error(`Field type is not resolved: ${field.$container.name}.${field.name}`); } const isArray = // typed-JSON fields should be translated to scalar Json type isTypeDef(field.type.reference?.ref) ? false : field.type.array; - const type = new ModelFieldType( - fieldType, - isArray, - field.type.optional - ); + const type = new ModelFieldType(fieldType, isArray, field.type.optional); const attributes = field.attributes .filter((attr) => this.isPrismaAttribute(attr)) @@ -272,18 +248,12 @@ export class PrismaSchemaGenerator { ModelUtils.isIdField(field) && this.isInheritedFromDelegate(field) && attr.decl.$refText === '@default' - ) + ), ) .map((attr) => this.makeFieldAttribute(attr)); const docs = [...field.comments]; - const result = model.addField( - field.name, - type, - attributes, - docs, - addToFront - ); + const result = model.addField(field.name, type, attributes, docs, addToFront); return result; } @@ -297,72 +267,45 @@ export class PrismaSchemaGenerator { return false; } - return AstUtils.streamAst(expr).some( - (node) => - isInvocationExpr(node) && this.isFromPlugin(node.function.ref) - ); + return AstUtils.streamAst(expr).some((node) => isInvocationExpr(node) && this.isFromPlugin(node.function.ref)); } private isFromPlugin(node: AstNode | undefined) { const model = AstUtils.getContainerOfType(node, isModel); - return ( - !!model && - !!model.$document && - model.$document.uri.path.endsWith('plugin.zmodel') - ); + return !!model && !!model.$document && model.$document.uri.path.endsWith('plugin.zmodel'); } private setDummyDefault(result: ModelField, field: DataModelField) { const dummyDefaultValue = match(field.type.type) .with('String', () => new AttributeArgValue('String', '')) - .with( - P.union('Int', 'BigInt', 'Float', 'Decimal'), - () => new AttributeArgValue('Number', '0') - ) + .with(P.union('Int', 'BigInt', 'Float', 'Decimal'), () => new AttributeArgValue('Number', '0')) .with('Boolean', () => new AttributeArgValue('Boolean', 'false')) - .with( - 'DateTime', - () => - new AttributeArgValue( - 'FunctionCall', - new PrismaFunctionCall('now') - ) - ) + .with('DateTime', () => new AttributeArgValue('FunctionCall', new PrismaFunctionCall('now'))) .with('Json', () => new AttributeArgValue('String', '{}')) .with('Bytes', () => new AttributeArgValue('String', '')) .otherwise(() => { - throw new Error( - `Unsupported field type with default value: ${field.type.type}` - ); + throw new Error(`Unsupported field type with default value: ${field.type.type}`); }); result.attributes.push( - new PrismaFieldAttribute('@default', [ - new PrismaAttributeArg(undefined, dummyDefaultValue), - ]) + new PrismaFieldAttribute('@default', [new PrismaAttributeArg(undefined, dummyDefaultValue)]), ); } private isInheritedFromDelegate(field: DataModelField) { - return ( - field.$inheritedFrom && - ModelUtils.isDelegateModel(field.$inheritedFrom) - ); + return field.$inheritedFrom && ModelUtils.isDelegateModel(field.$inheritedFrom); } private makeFieldAttribute(attr: DataModelFieldAttribute) { const attrName = attr.decl.ref!.name; return new PrismaFieldAttribute( attrName, - attr.args.map((arg) => this.makeAttributeArg(arg)) + attr.args.map((arg) => this.makeAttributeArg(arg)), ); } private makeAttributeArg(arg: AttributeArg): PrismaAttributeArg { - return new PrismaAttributeArg( - arg.name, - this.makeAttributeArgValue(arg.value) - ); + return new PrismaAttributeArg(arg.name, this.makeAttributeArgValue(arg.value)); } private makeAttributeArgValue(node: Expression): PrismaAttributeArgValue { @@ -376,36 +319,21 @@ export class PrismaSchemaGenerator { } else if (isArrayExpr(node)) { return new PrismaAttributeArgValue( 'Array', - new Array( - ...node.items.map((item) => - this.makeAttributeArgValue(item) - ) - ) + new Array(...node.items.map((item) => this.makeAttributeArgValue(item))), ); } else if (isReferenceExpr(node)) { return new PrismaAttributeArgValue( 'FieldReference', new PrismaFieldReference( node.target.ref!.name, - node.args.map( - (arg) => - new PrismaFieldReferenceArg( - arg.name, - this.exprToText(arg.value) - ) - ) - ) + node.args.map((arg) => new PrismaFieldReferenceArg(arg.name, this.exprToText(arg.value))), + ), ); } else if (isInvocationExpr(node)) { // invocation - return new PrismaAttributeArgValue( - 'FunctionCall', - this.makeFunctionCall(node) - ); + return new PrismaAttributeArgValue('FunctionCall', this.makeFunctionCall(node)); } else { - throw Error( - `Unsupported attribute argument expression type: ${node.$type}` - ); + throw Error(`Unsupported attribute argument expression type: ${node.$type}`); } } @@ -422,26 +350,21 @@ export class PrismaSchemaGenerator { .when(isLiteralExpr, (v) => v.value.toString()) .when(isNullExpr, () => 'null') .otherwise(() => { - throw new Error( - 'Function call argument must be literal or null' - ); + throw new Error('Function call argument must be literal or null'); }); return new PrismaFunctionCallArg(val); - }) + }), ); } - private generateContainerAttribute( - container: PrismaContainerDeclaration, - attr: DataModelAttribute - ) { + private generateContainerAttribute(container: PrismaContainerDeclaration, attr: DataModelAttribute) { const attrName = attr.decl.ref!.name; container.attributes.push( new PrismaModelAttribute( attrName, - attr.args.map((arg) => this.makeAttributeArg(arg)) - ) + attr.args.map((arg) => this.makeAttributeArg(arg)), + ), ); } @@ -452,9 +375,7 @@ export class PrismaSchemaGenerator { this.generateEnumField(_enum, field); } - for (const attr of decl.attributes.filter((attr) => - this.isPrismaAttribute(attr) - )) { + for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) { this.generateContainerAttribute(_enum, attr); } diff --git a/packages/sdk/src/schema/expression.ts b/packages/sdk/src/schema/expression.ts index dafc30f5..3ce3c2d1 100644 --- a/packages/sdk/src/schema/expression.ts +++ b/packages/sdk/src/schema/expression.ts @@ -58,16 +58,4 @@ export type NullExpression = { }; export type UnaryOperator = '!'; -export type BinaryOperator = - | '&&' - | '||' - | '==' - | '!=' - | '<' - | '<=' - | '>' - | '>=' - | '?' - | '!' - | '^' - | 'in'; +export type BinaryOperator = '&&' | '||' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '?' | '!' | '^' | 'in'; diff --git a/packages/sdk/src/schema/schema.ts b/packages/sdk/src/schema/schema.ts index 9aa89dd6..d6242e68 100644 --- a/packages/sdk/src/schema/schema.ts +++ b/packages/sdk/src/schema/schema.ts @@ -27,7 +27,6 @@ export type ModelDef = { | Record> >; idFields: string[]; - // eslint-disable-next-line @typescript-eslint/ban-types computedFields?: Record; }; @@ -41,12 +40,7 @@ export type AttributeArg = { value: Expression; }; -export type CascadeAction = - | 'SetNull' - | 'Cascade' - | 'Restrict' - | 'NoAction' - | 'SetDefault'; +export type CascadeAction = 'SetNull' | 'Cascade' | 'Restrict' | 'NoAction' | 'SetDefault'; export type RelationInfo = { name?: string; @@ -79,179 +73,119 @@ export type ProcedureDef = { mutation?: boolean; }; -export type BuiltinType = - | 'String' - | 'Boolean' - | 'Int' - | 'Float' - | 'BigInt' - | 'Decimal' - | 'DateTime' - | 'Bytes'; - -export type MappedBuiltinType = - | string - | boolean - | number - | bigint - | Decimal - | Date; +export type BuiltinType = 'String' | 'Boolean' | 'Int' | 'Float' | 'BigInt' | 'Decimal' | 'DateTime' | 'Bytes'; + +export type MappedBuiltinType = string | boolean | number | bigint | Decimal | Date; export type EnumDef = Record; //#region Extraction -export type GetModels = Extract< - keyof Schema['models'], - string ->; +export type GetModels = Extract; -export type GetModel< - Schema extends SchemaDef, - Model extends GetModels -> = Schema['models'][Model]; +export type GetModel> = Schema['models'][Model]; export type GetEnums = keyof Schema['enums']; -export type GetEnum< - Schema extends SchemaDef, - Enum extends GetEnums -> = Schema['enums'][Enum]; +export type GetEnum> = Schema['enums'][Enum]; -export type GetFields< - Schema extends SchemaDef, - Model extends GetModels -> = Extract['fields'], string>; +export type GetFields> = Extract< + keyof GetModel['fields'], + string +>; export type GetField< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = Schema['models'][Model]['fields'][Field]; export type GetFieldType< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = Schema['models'][Model]['fields'][Field]['type']; export type ScalarFields< Schema extends SchemaDef, Model extends GetModels, - IncludeComputed extends boolean = true + IncludeComputed extends boolean = true, > = keyof { - [Key in GetFields as GetField< - Schema, - Model, - Key - >['relation'] extends object + [Key in GetFields as GetField['relation'] extends object ? never : GetField['foreignKeyFor'] extends string[] - ? never - : IncludeComputed extends true - ? Key - : FieldIsComputed extends true - ? never - : Key]: Key; + ? never + : IncludeComputed extends true + ? Key + : FieldIsComputed extends true + ? never + : Key]: Key; }; -export type ForeignKeyFields< - Schema extends SchemaDef, - Model extends GetModels -> = keyof { - [Key in GetFields as GetField< - Schema, - Model, - Key - >['foreignKeyFor'] extends string[] +export type ForeignKeyFields> = keyof { + [Key in GetFields as GetField['foreignKeyFor'] extends string[] ? Key : never]: Key; }; -export type NonRelationFields< - Schema extends SchemaDef, - Model extends GetModels -> = keyof { - [Key in GetFields as GetField< - Schema, - Model, - Key - >['relation'] extends object - ? never - : Key]: Key; +export type NonRelationFields> = keyof { + [Key in GetFields as GetField['relation'] extends object ? never : Key]: Key; }; -export type RelationFields< - Schema extends SchemaDef, - Model extends GetModels -> = keyof { - [Key in GetFields as GetField< - Schema, - Model, - Key - >['relation'] extends object - ? Key - : never]: Key; +export type RelationFields> = keyof { + [Key in GetFields as GetField['relation'] extends object ? Key : never]: Key; }; export type FieldType< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = GetField['type']; export type RelationFieldType< Schema extends SchemaDef, Model extends GetModels, - Field extends RelationFields -> = GetField['type'] extends GetModels - ? GetField['type'] - : never; + Field extends RelationFields, +> = GetField['type'] extends GetModels ? GetField['type'] : never; export type FieldIsOptional< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = GetField['optional'] extends true ? true : false; export type FieldIsRelation< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = GetField['relation'] extends object ? true : false; export type FieldIsArray< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = GetField['array'] extends true ? true : false; export type FieldIsComputed< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields + Field extends GetFields, > = GetField['computed'] extends true ? true : false; export type FieldHasDefault< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields -> = GetField['default'] extends - | object - | number - | string - | boolean + Field extends GetFields, +> = GetField['default'] extends object | number | string | boolean ? true : GetField['updatedAt'] extends true - ? true - : false; + ? true + : false; export type FieldIsRelationArray< Schema extends SchemaDef, Model extends GetModels, - Field extends GetFields -> = FieldIsRelation extends true - ? FieldIsArray - : false; + Field extends GetFields, +> = FieldIsRelation extends true ? FieldIsArray : false; //#endregion diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index ac7caf23..1b1f6c56 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -38,20 +38,10 @@ import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import * as ts from 'typescript'; import { ModelUtils } from '.'; -import { - getAttribute, - getAuthDecl, - hasAttribute, - isIdField, - isUniqueField, -} from './model-utils'; +import { getAttribute, getAuthDecl, hasAttribute, isIdField, isUniqueField } from './model-utils'; export class TsSchemaGenerator { - public async generate( - schemaFile: string, - pluginModelFiles: string[], - outputFile: string - ) { + public async generate(schemaFile: string, pluginModelFiles: string[], outputFile: string) { const loaded = await loadDocument(schemaFile, pluginModelFiles); if (!loaded.success) { throw new Error(`Error loading schema:${loaded.errors.join('\n')}`); @@ -64,19 +54,9 @@ export class TsSchemaGenerator { this.generateBannerComments(statements); - const sourceFile = ts.createSourceFile( - outputFile, - '', - ts.ScriptTarget.ESNext, - false, - ts.ScriptKind.TS - ); + const sourceFile = ts.createSourceFile(outputFile, '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); const printer = ts.createPrinter(); - const result = printer.printList( - ts.ListFormat.MultiLine, - ts.factory.createNodeArray(statements), - sourceFile - ); + const result = printer.printList(ts.ListFormat.MultiLine, ts.factory.createNodeArray(statements), sourceFile); fs.mkdirSync(path.dirname(outputFile), { recursive: true }); fs.writeFileSync(outputFile, result); @@ -86,9 +66,7 @@ export class TsSchemaGenerator { private generateSchemaStatements(model: Model, statements: ts.Statement[]) { const hasComputedFields = model.declarations.some( - (d) => - isDataModel(d) && - d.fields.some((f) => hasAttribute(f, '@computed')) + (d) => isDataModel(d) && d.fields.some((f) => hasAttribute(f, '@computed')), ); const runtimeImportDecl = ts.factory.createImportDeclaration( @@ -97,30 +75,20 @@ export class TsSchemaGenerator { false, undefined, ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - true, - undefined, - ts.factory.createIdentifier('SchemaDef') - ), + ts.factory.createImportSpecifier(true, undefined, ts.factory.createIdentifier('SchemaDef')), ...(hasComputedFields ? [ ts.factory.createImportSpecifier( true, undefined, - ts.factory.createIdentifier( - 'OperandExpression' - ) + ts.factory.createIdentifier('OperandExpression'), ), ] : []), - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier('ExpressionUtils') - ), - ]) + ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier('ExpressionUtils')), + ]), ), - ts.factory.createStringLiteral('@zenstackhq/runtime/schema') + ts.factory.createStringLiteral('@zenstackhq/runtime/schema'), ); statements.push(runtimeImportDecl); @@ -135,14 +103,14 @@ export class TsSchemaGenerator { ts.factory.createSatisfiesExpression( ts.factory.createAsExpression( this.createSchemaObject(model), - ts.factory.createTypeReferenceNode('const') + ts.factory.createTypeReferenceNode('const'), ), - ts.factory.createTypeReferenceNode('SchemaDef') - ) + ts.factory.createTypeReferenceNode('SchemaDef'), + ), ), ], - ts.NodeFlags.Const - ) + ts.NodeFlags.Const, + ), ); statements.push(declaration); @@ -151,7 +119,7 @@ export class TsSchemaGenerator { [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'SchemaType', undefined, - ts.factory.createTypeReferenceNode('typeof schema') + ts.factory.createTypeReferenceNode('typeof schema'), ); statements.push(typeDeclaration); } @@ -159,16 +127,10 @@ export class TsSchemaGenerator { private createSchemaObject(model: Model) { const properties: ts.PropertyAssignment[] = [ // provider - ts.factory.createPropertyAssignment( - 'provider', - this.createProviderObject(model) - ), + ts.factory.createPropertyAssignment('provider', this.createProviderObject(model)), // models - ts.factory.createPropertyAssignment( - 'models', - this.createModelsObject(model) - ), + ts.factory.createPropertyAssignment('models', this.createModelsObject(model)), ]; // enums @@ -178,46 +140,28 @@ export class TsSchemaGenerator { ts.factory.createPropertyAssignment( 'enums', ts.factory.createObjectLiteralExpression( - enums.map((e) => - ts.factory.createPropertyAssignment( - e.name, - this.createEnumObject(e) - ) - ), - true - ) - ) + enums.map((e) => ts.factory.createPropertyAssignment(e.name, this.createEnumObject(e))), + true, + ), + ), ); } // authType const authType = getAuthDecl(model); if (authType) { - properties.push( - ts.factory.createPropertyAssignment( - 'authType', - this.createLiteralNode(authType.name) - ) - ); + properties.push(ts.factory.createPropertyAssignment('authType', this.createLiteralNode(authType.name))); } // procedures const procedures = model.declarations.filter(isProcedure); if (procedures.length > 0) { - properties.push( - ts.factory.createPropertyAssignment( - 'procedures', - this.createProceduresObject(procedures) - ) - ); + properties.push(ts.factory.createPropertyAssignment('procedures', this.createProceduresObject(procedures))); } // plugins properties.push( - ts.factory.createPropertyAssignment( - 'plugins', - ts.factory.createObjectLiteralExpression([], true) - ) + ts.factory.createPropertyAssignment('plugins', ts.factory.createObjectLiteralExpression([], true)), ); return ts.factory.createObjectLiteralExpression(properties, true); @@ -226,30 +170,17 @@ export class TsSchemaGenerator { private createProviderObject(model: Model): ts.Expression { const dsProvider = this.getDataSourceProvider(model); return ts.factory.createObjectLiteralExpression( - [ - ts.factory.createPropertyAssignment( - 'type', - ts.factory.createStringLiteral(dsProvider.type) - ), - ], - true + [ts.factory.createPropertyAssignment('type', ts.factory.createStringLiteral(dsProvider.type))], + true, ); } private createModelsObject(model: Model) { return ts.factory.createObjectLiteralExpression( model.declarations - .filter( - (d): d is DataModel => - isDataModel(d) && !hasAttribute(d, '@@ignore') - ) - .map((dm) => - ts.factory.createPropertyAssignment( - dm.name, - this.createDataModelObject(dm) - ) - ), - true + .filter((d): d is DataModel => isDataModel(d) && !hasAttribute(d, '@@ignore')) + .map((dm) => ts.factory.createPropertyAssignment(dm.name, this.createDataModelObject(dm))), + true, ); } @@ -262,13 +193,10 @@ export class TsSchemaGenerator { dm.fields .filter((field) => !hasAttribute(field, '@ignore')) .map((field) => - ts.factory.createPropertyAssignment( - field.name, - this.createDataModelFieldObject(field) - ) + ts.factory.createPropertyAssignment(field.name, this.createDataModelFieldObject(field)), ), - true - ) + true, + ), ), // attributes @@ -277,11 +205,9 @@ export class TsSchemaGenerator { ts.factory.createPropertyAssignment( 'attributes', ts.factory.createArrayLiteralExpression( - dm.attributes.map((attr) => - this.createAttributeObject(attr) - ), - true - ) + dm.attributes.map((attr) => this.createAttributeObject(attr)), + true, + ), ), ] : []), @@ -290,29 +216,19 @@ export class TsSchemaGenerator { ts.factory.createPropertyAssignment( 'idFields', ts.factory.createArrayLiteralExpression( - this.getIdFields(dm).map((idField) => - ts.factory.createStringLiteral(idField) - ) - ) + this.getIdFields(dm).map((idField) => ts.factory.createStringLiteral(idField)), + ), ), // uniqueFields - ts.factory.createPropertyAssignment( - 'uniqueFields', - this.createUniqueFieldsObject(dm) - ), + ts.factory.createPropertyAssignment('uniqueFields', this.createUniqueFieldsObject(dm)), ]; - const computedFields = dm.fields.filter((f) => - hasAttribute(f, '@computed') - ); + const computedFields = dm.fields.filter((f) => hasAttribute(f, '@computed')); if (computedFields.length > 0) { fields.push( - ts.factory.createPropertyAssignment( - 'computedFields', - this.createComputedFieldsObject(computedFields) - ) + ts.factory.createPropertyAssignment('computedFields', this.createComputedFieldsObject(computedFields)), ); } @@ -330,29 +246,21 @@ export class TsSchemaGenerator { undefined, [], ts.factory.createTypeReferenceNode('OperandExpression', [ - ts.factory.createKeywordTypeNode( - this.mapTypeToTSSyntaxKeyword(field.type.type!) - ), + ts.factory.createKeywordTypeNode(this.mapTypeToTSSyntaxKeyword(field.type.type!)), ]), ts.factory.createBlock( [ ts.factory.createThrowStatement( - ts.factory.createNewExpression( - ts.factory.createIdentifier('Error'), - undefined, - [ - ts.factory.createStringLiteral( - 'This is a stub for computed field' - ), - ] - ) + ts.factory.createNewExpression(ts.factory.createIdentifier('Error'), undefined, [ + ts.factory.createStringLiteral('This is a stub for computed field'), + ]), ), ], - true - ) - ) + true, + ), + ), ), - true + true, ); } @@ -371,55 +279,28 @@ export class TsSchemaGenerator { const objectFields = [ ts.factory.createPropertyAssignment( 'type', - ts.factory.createStringLiteral( - field.type.type ?? field.type.reference!.$refText - ) + ts.factory.createStringLiteral(field.type.type ?? field.type.reference!.$refText), ), ]; if (isIdField(field)) { - objectFields.push( - ts.factory.createPropertyAssignment( - 'id', - ts.factory.createTrue() - ) - ); + objectFields.push(ts.factory.createPropertyAssignment('id', ts.factory.createTrue())); } if (isUniqueField(field)) { - objectFields.push( - ts.factory.createPropertyAssignment( - 'unique', - ts.factory.createTrue() - ) - ); + objectFields.push(ts.factory.createPropertyAssignment('unique', ts.factory.createTrue())); } if (field.type.optional) { - objectFields.push( - ts.factory.createPropertyAssignment( - 'optional', - ts.factory.createTrue() - ) - ); + objectFields.push(ts.factory.createPropertyAssignment('optional', ts.factory.createTrue())); } if (field.type.array) { - objectFields.push( - ts.factory.createPropertyAssignment( - 'array', - ts.factory.createTrue() - ) - ); + objectFields.push(ts.factory.createPropertyAssignment('array', ts.factory.createTrue())); } if (hasAttribute(field, '@updatedAt')) { - objectFields.push( - ts.factory.createPropertyAssignment( - 'updatedAt', - ts.factory.createTrue() - ) - ); + objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ts.factory.createTrue())); } // attributes @@ -428,11 +309,9 @@ export class TsSchemaGenerator { ts.factory.createPropertyAssignment( 'attributes', ts.factory.createArrayLiteralExpression( - field.attributes.map((attr) => - this.createAttributeObject(attr) - ) - ) - ) + field.attributes.map((attr) => this.createAttributeObject(attr)), + ), + ), ); } @@ -445,59 +324,43 @@ export class TsSchemaGenerator { 'default', ts.factory.createCallExpression( - ts.factory.createIdentifier( - 'ExpressionUtils.call' - ), + ts.factory.createIdentifier('ExpressionUtils.call'), undefined, [ - ts.factory.createStringLiteral( - defaultValue.call - ), + ts.factory.createStringLiteral(defaultValue.call), ...(defaultValue.args.length > 0 ? [ ts.factory.createArrayLiteralExpression( - defaultValue.args.map((arg) => - this.createLiteralNode( - arg - ) - ) + defaultValue.args.map((arg) => this.createLiteralNode(arg)), ), ] : []), - ] - ) - ) + ], + ), + ), ); } else if ('authMember' in defaultValue) { objectFields.push( ts.factory.createPropertyAssignment( 'default', ts.factory.createCallExpression( - ts.factory.createIdentifier( - 'ExpressionUtils.member' - ), + ts.factory.createIdentifier('ExpressionUtils.member'), undefined, [ ts.factory.createCallExpression( - ts.factory.createIdentifier( - 'ExpressionUtils.call' - ), + ts.factory.createIdentifier('ExpressionUtils.call'), undefined, - [ts.factory.createStringLiteral('auth')] + [ts.factory.createStringLiteral('auth')], ), ts.factory.createArrayLiteralExpression( - defaultValue.authMember.map((m) => - ts.factory.createStringLiteral(m) - ) + defaultValue.authMember.map((m) => ts.factory.createStringLiteral(m)), ), - ] - ) - ) + ], + ), + ), ); } else { - throw new Error( - `Unsupported default value type for field ${field.name}` - ); + throw new Error(`Unsupported default value type for field ${field.name}`); } } else { objectFields.push( @@ -506,31 +369,21 @@ export class TsSchemaGenerator { typeof defaultValue === 'string' ? ts.factory.createStringLiteral(defaultValue) : typeof defaultValue === 'number' - ? ts.factory.createNumericLiteral(defaultValue) - : defaultValue === true - ? ts.factory.createTrue() - : ts.factory.createFalse() - ) + ? ts.factory.createNumericLiteral(defaultValue) + : defaultValue === true + ? ts.factory.createTrue() + : ts.factory.createFalse(), + ), ); } } if (hasAttribute(field, '@computed')) { - objectFields.push( - ts.factory.createPropertyAssignment( - 'computed', - ts.factory.createTrue() - ) - ); + objectFields.push(ts.factory.createPropertyAssignment('computed', ts.factory.createTrue())); } if (isDataModel(field.type.reference?.ref)) { - objectFields.push( - ts.factory.createPropertyAssignment( - 'relation', - this.createRelationObject(field) - ) - ); + objectFields.push(ts.factory.createPropertyAssignment('relation', this.createRelationObject(field))); } const fkFor = this.getForeignKeyFor(field); @@ -540,9 +393,9 @@ export class TsSchemaGenerator { 'foreignKeyFor', ts.factory.createArrayLiteralExpression( fkFor.map((fk) => ts.factory.createStringLiteral(fk)), - true - ) - ) + true, + ), + ), ); } @@ -550,36 +403,23 @@ export class TsSchemaGenerator { } private getDataSourceProvider( - model: Model - ): - | { type: string; env: undefined; url: string } - | { type: string; env: string; url: undefined } { + model: Model, + ): { type: string; env: undefined; url: string } | { type: string; env: string; url: undefined } { const dataSource = model.declarations.find(isDataSource); invariant(dataSource, 'No data source found in the model'); - const providerExpr = dataSource.fields.find( - (f) => f.name === 'provider' - )?.value; + const providerExpr = dataSource.fields.find((f) => f.name === 'provider')?.value; invariant(isLiteralExpr(providerExpr), 'Provider must be a literal'); const type = providerExpr.value as string; const urlExpr = dataSource.fields.find((f) => f.name === 'url')?.value; - invariant( - isLiteralExpr(urlExpr) || isInvocationExpr(urlExpr), - 'URL must be a literal or env function' - ); + invariant(isLiteralExpr(urlExpr) || isInvocationExpr(urlExpr), 'URL must be a literal or env function'); if (isLiteralExpr(urlExpr)) { return { type, url: urlExpr.value as string, env: undefined }; } else if (isInvocationExpr(urlExpr)) { - invariant( - urlExpr.function.$refText === 'env', - 'only "env" function is supported' - ); - invariant( - urlExpr.args.length === 1, - 'env function must have one argument' - ); + invariant(urlExpr.function.$refText === 'env', 'only "env" function is supported'); + invariant(urlExpr.args.length === 1, 'env function must have one argument'); return { type, env: (urlExpr.args[0]!.value as LiteralExpr).value as string, @@ -591,14 +431,8 @@ export class TsSchemaGenerator { } private getMappedDefault( - field: DataModelField - ): - | string - | number - | boolean - | { call: string; args: any[] } - | { authMember: string[] } - | undefined { + field: DataModelField, + ): string | number | boolean | { call: string; args: any[] } | { authMember: string[] } | undefined { const defaultAttr = getAttribute(field, '@default'); if (!defaultAttr) { return undefined; @@ -611,31 +445,22 @@ export class TsSchemaGenerator { const lit = (defaultValue as LiteralExpr).value; return field.type.type === 'Boolean' ? (lit as boolean) - : ['Int', 'Float', 'Decimal', 'BigInt'].includes( - field.type.type! - ) - ? Number(lit) - : lit; - } else if ( - isReferenceExpr(defaultValue) && - isEnumField(defaultValue.target.ref) - ) { + : ['Int', 'Float', 'Decimal', 'BigInt'].includes(field.type.type!) + ? Number(lit) + : lit; + } else if (isReferenceExpr(defaultValue) && isEnumField(defaultValue.target.ref)) { return defaultValue.target.ref.name; } else if (isInvocationExpr(defaultValue)) { return { call: defaultValue.function.$refText, - args: defaultValue.args.map((arg) => - this.getLiteral(arg.value) - ), + args: defaultValue.args.map((arg) => this.getLiteral(arg.value)), }; } else if (this.isAuthMemberAccess(defaultValue)) { return { authMember: this.getMemberAccessChain(defaultValue), }; } else { - throw new Error( - `Unsupported default value type for field ${field.name}` - ); + throw new Error(`Unsupported default value type for field ${field.name}`); } } @@ -643,19 +468,13 @@ export class TsSchemaGenerator { if (!isMemberAccessExpr(expr.operand)) { return [expr.member.$refText]; } else { - return [ - ...this.getMemberAccessChain(expr.operand), - expr.member.$refText, - ]; + return [...this.getMemberAccessChain(expr.operand), expr.member.$refText]; } } private isAuthMemberAccess(expr: Expression): expr is MemberAccessExpr { if (isMemberAccessExpr(expr)) { - return ( - this.isAuthInvocation(expr.operand) || - this.isAuthMemberAccess(expr.operand) - ); + return this.isAuthInvocation(expr.operand) || this.isAuthMemberAccess(expr.operand); } else { return false; } @@ -663,9 +482,7 @@ export class TsSchemaGenerator { private isAuthInvocation(expr: Expression) { return ( - isInvocationExpr(expr) && - expr.function.$refText === 'auth' && - ModelUtils.isFromStdlib(expr.function.ref!) + isInvocationExpr(expr) && expr.function.$refText === 'auth' && ModelUtils.isFromStdlib(expr.function.ref!) ); } @@ -675,20 +492,14 @@ export class TsSchemaGenerator { const oppositeRelation = this.getOppositeRelationField(field); if (oppositeRelation) { relationFields.push( - ts.factory.createPropertyAssignment( - 'opposite', - ts.factory.createStringLiteral(oppositeRelation.name) - ) + ts.factory.createPropertyAssignment('opposite', ts.factory.createStringLiteral(oppositeRelation.name)), ); } const relationName = this.getRelationName(field); if (relationName) { relationFields.push( - ts.factory.createPropertyAssignment( - 'name', - ts.factory.createStringLiteral(relationName) - ) + ts.factory.createPropertyAssignment('name', ts.factory.createStringLiteral(relationName)), ); } @@ -703,11 +514,9 @@ export class TsSchemaGenerator { ts.factory.createPropertyAssignment( param, ts.factory.createArrayLiteralExpression( - fieldNames.map((el) => - ts.factory.createStringLiteral(el) - ) - ) - ) + fieldNames.map((el) => ts.factory.createStringLiteral(el)), + ), + ), ); } } @@ -715,10 +524,7 @@ export class TsSchemaGenerator { if (param === 'onDelete' || param === 'onUpdate') { const action = (arg.value as ReferenceExpr).target.$refText; relationFields.push( - ts.factory.createPropertyAssignment( - param, - ts.factory.createStringLiteral(action) - ) + ts.factory.createPropertyAssignment(param, ts.factory.createStringLiteral(action)), ); } } @@ -728,10 +534,7 @@ export class TsSchemaGenerator { } private getReferenceNames(expr: Expression) { - return ( - isArrayExpr(expr) && - expr.items.map((item) => (item as ReferenceExpr).target.$refText) - ); + return isArrayExpr(expr) && expr.items.map((item) => (item as ReferenceExpr).target.$refText); } private getForeignKeyFor(field: DataModelField) { @@ -743,10 +546,7 @@ export class TsSchemaGenerator { if ( arg.name === 'fields' && isArrayExpr(arg.value) && - arg.value.items.some( - (el) => - isReferenceExpr(el) && el.target.ref === field - ) + arg.value.items.some((el) => isReferenceExpr(el) && el.target.ref === field) ) { result.push(f.name); } @@ -757,10 +557,7 @@ export class TsSchemaGenerator { } private getOppositeRelationField(field: DataModelField) { - if ( - !field.type.reference?.ref || - !isDataModel(field.type.reference?.ref) - ) { + if (!field.type.reference?.ref || !isDataModel(field.type.reference?.ref)) { return undefined; } @@ -790,14 +587,9 @@ export class TsSchemaGenerator { private getRelationName(field: DataModelField) { const relation = getAttribute(field, '@relation'); if (relation) { - const nameArg = relation.args.find( - (arg) => arg.$resolvedParam.name === 'name' - ); + const nameArg = relation.args.find((arg) => arg.$resolvedParam.name === 'name'); if (nameArg) { - invariant( - isLiteralExpr(nameArg.value), - 'name must be a literal' - ); + invariant(isLiteralExpr(nameArg.value), 'name must be a literal'); return nameArg.value.value as string; } } @@ -820,20 +612,17 @@ export class TsSchemaGenerator { ts.factory.createObjectLiteralExpression([ ts.factory.createPropertyAssignment( 'type', - ts.factory.createStringLiteral(field.type.type!) + ts.factory.createStringLiteral(field.type.type!), ), - ]) - ) + ]), + ), ); } } // model-level id and unique for (const attr of dm.attributes) { - if ( - attr.decl.$refText === '@@id' || - attr.decl.$refText === '@@unique' - ) { + if (attr.decl.$refText === '@@id' || attr.decl.$refText === '@@unique') { const fieldNames = this.getReferenceNames(attr.args[0]!.value); if (!fieldNames) { continue; @@ -841,21 +630,17 @@ export class TsSchemaGenerator { if (fieldNames.length === 1) { // single-field unique - const fieldDef = dm.fields.find( - (f) => f.name === fieldNames[0] - )!; + const fieldDef = dm.fields.find((f) => f.name === fieldNames[0])!; properties.push( ts.factory.createPropertyAssignment( fieldNames[0]!, ts.factory.createObjectLiteralExpression([ ts.factory.createPropertyAssignment( 'type', - ts.factory.createStringLiteral( - fieldDef.type.type! - ) + ts.factory.createStringLiteral(fieldDef.type.type!), ), - ]) - ) + ]), + ), ); } else { // multi-field unique @@ -864,25 +649,19 @@ export class TsSchemaGenerator { fieldNames.join('_'), ts.factory.createObjectLiteralExpression( fieldNames.map((field) => { - const fieldDef = dm.fields.find( - (f) => f.name === field - )!; + const fieldDef = dm.fields.find((f) => f.name === field)!; return ts.factory.createPropertyAssignment( field, - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createPropertyAssignment( - 'type', - ts.factory.createStringLiteral( - fieldDef.type.type! - ) - ), - ] - ) + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + 'type', + ts.factory.createStringLiteral(fieldDef.type.type!), + ), + ]), ); - }) - ) - ) + }), + ), + ), ); } } @@ -894,12 +673,9 @@ export class TsSchemaGenerator { private createEnumObject(e: Enum) { return ts.factory.createObjectLiteralExpression( e.fields.map((field) => - ts.factory.createPropertyAssignment( - field.name, - ts.factory.createStringLiteral(field.name) - ) + ts.factory.createPropertyAssignment(field.name, ts.factory.createStringLiteral(field.name)), ), - true + true, ); } @@ -922,25 +698,20 @@ export class TsSchemaGenerator { return arg === null ? ts.factory.createNull() : typeof arg === 'string' - ? ts.factory.createStringLiteral(arg) - : typeof arg === 'number' - ? ts.factory.createNumericLiteral(arg) - : arg === true - ? ts.factory.createTrue() - : arg === false - ? ts.factory.createFalse() - : undefined; + ? ts.factory.createStringLiteral(arg) + : typeof arg === 'number' + ? ts.factory.createNumericLiteral(arg) + : arg === true + ? ts.factory.createTrue() + : arg === false + ? ts.factory.createFalse() + : undefined; } private createProceduresObject(procedures: Procedure[]) { return ts.factory.createObjectLiteralExpression( - procedures.map((proc) => - ts.factory.createPropertyAssignment( - proc.name, - this.createProcedureObject(proc) - ) - ), - true + procedures.map((proc) => ts.factory.createPropertyAssignment(proc.name, this.createProcedureObject(proc))), + true, ); } @@ -948,27 +719,17 @@ export class TsSchemaGenerator { const params = ts.factory.createArrayLiteralExpression( proc.params.map((param) => ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - 'name', - ts.factory.createStringLiteral(param.name) - ), + ts.factory.createPropertyAssignment('name', ts.factory.createStringLiteral(param.name)), ...(param.optional - ? [ - ts.factory.createPropertyAssignment( - 'optional', - ts.factory.createTrue() - ), - ] + ? [ts.factory.createPropertyAssignment('optional', ts.factory.createTrue())] : []), ts.factory.createPropertyAssignment( 'type', - ts.factory.createStringLiteral( - param.type.type ?? param.type.reference!.$refText - ) + ts.factory.createStringLiteral(param.type.type ?? param.type.reference!.$refText), ), - ]) + ]), ), - true + true, ); const paramsType = ts.factory.createTupleTypeNode([ @@ -982,63 +743,41 @@ export class TsSchemaGenerator { undefined, ts.factory.createStringLiteral('name'), undefined, - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral(param.name) - ) + ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(param.name)), ), ts.factory.createPropertySignature( undefined, ts.factory.createStringLiteral('type'), undefined, ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral( - param.type.type ?? - param.type.reference!.$refText - ) - ) + ts.factory.createStringLiteral(param.type.type ?? param.type.reference!.$refText), + ), ), ...(param.optional ? [ ts.factory.createPropertySignature( undefined, - ts.factory.createStringLiteral( - 'optional' - ), + ts.factory.createStringLiteral('optional'), undefined, - ts.factory.createLiteralTypeNode( - ts.factory.createTrue() - ) + ts.factory.createLiteralTypeNode(ts.factory.createTrue()), ), ] : []), - ]) - ) + ]), + ), ), ]); return ts.factory.createObjectLiteralExpression( [ - ts.factory.createPropertyAssignment( - 'params', - ts.factory.createAsExpression(params, paramsType) - ), + ts.factory.createPropertyAssignment('params', ts.factory.createAsExpression(params, paramsType)), ts.factory.createPropertyAssignment( 'returnType', - ts.factory.createStringLiteral( - proc.returnType.type ?? - proc.returnType.reference!.$refText - ) + ts.factory.createStringLiteral(proc.returnType.type ?? proc.returnType.reference!.$refText), ), - ...(proc.mutation - ? [ - ts.factory.createPropertyAssignment( - 'mutation', - ts.factory.createTrue() - ), - ] - : []), + ...(proc.mutation ? [ts.factory.createPropertyAssignment('mutation', ts.factory.createTrue())] : []), ], - true + true, ); } @@ -1048,31 +787,20 @@ export class TsSchemaGenerator { // This file is automatically generated by ZenStack CLI and should not be manually updated. // ////////////////////////////////////////////////////////////////////////////////////////////// +/* eslint-disable */ + `; - ts.addSyntheticLeadingComment( - statements[0]!, - ts.SyntaxKind.SingleLineCommentTrivia, - banner - ); + ts.addSyntheticLeadingComment(statements[0]!, ts.SyntaxKind.SingleLineCommentTrivia, banner); } - private createAttributeObject( - attr: DataModelAttribute | DataModelFieldAttribute - ): ts.Expression { + private createAttributeObject(attr: DataModelAttribute | DataModelFieldAttribute): ts.Expression { return ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - 'name', - ts.factory.createStringLiteral(attr.decl.$refText) - ), + ts.factory.createPropertyAssignment('name', ts.factory.createStringLiteral(attr.decl.$refText)), ...(attr.args.length > 0 ? [ ts.factory.createPropertyAssignment( 'args', - ts.factory.createArrayLiteralExpression( - attr.args.map((arg) => - this.createAttributeArg(arg) - ) - ) + ts.factory.createArrayLiteralExpression(attr.args.map((arg) => this.createAttributeArg(arg))), ), ] : []), @@ -1083,52 +811,32 @@ export class TsSchemaGenerator { return ts.factory.createObjectLiteralExpression([ // name ...(arg.$resolvedParam?.name - ? [ - ts.factory.createPropertyAssignment( - 'name', - ts.factory.createStringLiteral( - arg.$resolvedParam.name - ) - ), - ] + ? [ts.factory.createPropertyAssignment('name', ts.factory.createStringLiteral(arg.$resolvedParam.name))] : []), // value - ts.factory.createPropertyAssignment( - 'value', - this.createExpression(arg.value) - ), + ts.factory.createPropertyAssignment('value', this.createExpression(arg.value)), ]); } private createExpression(value: Expression): ts.Expression { return match(value) - .when(isLiteralExpr, (expr) => - this.createLiteralExpression(expr.$type, expr.value) - ) + .when(isLiteralExpr, (expr) => this.createLiteralExpression(expr.$type, expr.value)) .when(isInvocationExpr, (expr) => this.createCallExpression(expr)) .when(isReferenceExpr, (expr) => this.createRefExpression(expr)) .when(isArrayExpr, (expr) => this.createArrayExpression(expr)) .when(isUnaryExpr, (expr) => this.createUnaryExpression(expr)) .when(isBinaryExpr, (expr) => this.createBinaryExpression(expr)) - .when(isMemberAccessExpr, (expr) => - this.createMemberExpression(expr) - ) + .when(isMemberAccessExpr, (expr) => this.createMemberExpression(expr)) .when(isNullExpr, () => this.createNullExpression()) .when(isThisExpr, () => this.createThisExpression()) .otherwise(() => { - throw new Error( - `Unsupported attribute arg value: ${value.$type}` - ); + throw new Error(`Unsupported attribute arg value: ${value.$type}`); }); } private createThisExpression() { - return ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils._this'), - undefined, - [] - ); + return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils._this'), undefined, []); } private createMemberExpression(expr: MemberAccessExpr) { @@ -1144,121 +852,74 @@ export class TsSchemaGenerator { const args = [ this.createExpression(receiver), - ts.factory.createArrayLiteralExpression( - members.map((m) => ts.factory.createStringLiteral(m)) - ), + ts.factory.createArrayLiteralExpression(members.map((m) => ts.factory.createStringLiteral(m))), ]; - return ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.member'), - undefined, - args - ); + return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.member'), undefined, args); } private createNullExpression() { - return ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils._null'), - undefined, - [] - ); + return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils._null'), undefined, []); } private createBinaryExpression(expr: BinaryExpr) { - return ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.binary'), - undefined, - [ - this.createExpression(expr.left), - this.createLiteralNode(expr.operator), - this.createExpression(expr.right), - ] - ); + return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.binary'), undefined, [ + this.createExpression(expr.left), + this.createLiteralNode(expr.operator), + this.createExpression(expr.right), + ]); } private createUnaryExpression(expr: UnaryExpr) { - return ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.unary'), - undefined, - [ - this.createLiteralNode(expr.operator), - this.createExpression(expr.operand), - ] - ); + return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.unary'), undefined, [ + this.createLiteralNode(expr.operator), + this.createExpression(expr.operand), + ]); } private createArrayExpression(expr: ArrayExpr): any { - return ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.array'), - undefined, - [ - ts.factory.createArrayLiteralExpression( - expr.items.map((item) => this.createExpression(item)) - ), - ] - ); + return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.array'), undefined, [ + ts.factory.createArrayLiteralExpression(expr.items.map((item) => this.createExpression(item))), + ]); } private createRefExpression(expr: ReferenceExpr): any { if (isDataModelField(expr.target.ref)) { - return ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.field'), - undefined, - [this.createLiteralNode(expr.target.$refText)] - ); + return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.field'), undefined, [ + this.createLiteralNode(expr.target.$refText), + ]); } else if (isEnumField(expr.target.ref)) { - return this.createLiteralExpression( - 'StringLiteral', - expr.target.$refText - ); + return this.createLiteralExpression('StringLiteral', expr.target.$refText); } else { - throw new Error( - `Unsupported reference type: ${expr.target.$refText}` - ); + throw new Error(`Unsupported reference type: ${expr.target.$refText}`); } } private createCallExpression(expr: InvocationExpr) { - return ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.call'), - undefined, - [ - ts.factory.createStringLiteral(expr.function.$refText), - ...(expr.args.length > 0 - ? [ - ts.factory.createArrayLiteralExpression( - expr.args.map((arg) => - this.createExpression(arg.value) - ) - ), - ] - : []), - ] - ); + return ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.call'), undefined, [ + ts.factory.createStringLiteral(expr.function.$refText), + ...(expr.args.length > 0 + ? [ts.factory.createArrayLiteralExpression(expr.args.map((arg) => this.createExpression(arg.value)))] + : []), + ]); } private createLiteralExpression(type: string, value: string | boolean) { return match(type) .with('BooleanLiteral', () => - ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.literal'), - undefined, - [this.createLiteralNode(value)] - ) + ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [ + this.createLiteralNode(value), + ]), ) .with('NumberLiteral', () => - ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.literal'), - undefined, - [ts.factory.createIdentifier(value as string)] - ) + ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [ + ts.factory.createIdentifier(value as string), + ]), ) .with('StringLiteral', () => - ts.factory.createCallExpression( - ts.factory.createIdentifier('ExpressionUtils.literal'), - undefined, - [this.createLiteralNode(value)] - ) + ts.factory.createCallExpression(ts.factory.createIdentifier('ExpressionUtils.literal'), undefined, [ + this.createLiteralNode(value), + ]), ) .otherwise(() => { throw new Error(`Unsupported literal type: ${type}`); diff --git a/packages/sdk/src/zmodel-code-generator.ts b/packages/sdk/src/zmodel-code-generator.ts index 4cf6cebe..ee115bee 100644 --- a/packages/sdk/src/zmodel-code-generator.ts +++ b/packages/sdk/src/zmodel-code-generator.ts @@ -60,11 +60,7 @@ const generationHandlers = new Map(); // generation handler decorator function gen(name: string) { - return function ( - _target: unknown, - _propertyKey: string, - descriptor: PropertyDescriptor - ) { + return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) { if (!generationHandlers.get(name)) { generationHandlers.set(name, descriptor); } @@ -120,9 +116,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} @gen(EnumField) private _generateEnumField(ast: EnumField) { return `${ast.name}${ - ast.attributes.length > 0 - ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') - : '' + ast.attributes.length > 0 ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : '' }`; } @@ -149,10 +143,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} return ast.name; } else { return `${ast.name}(${ast.args - .map( - (x) => - (x.name ? x.name + ': ' : '') + this.generate(x.value) - ) + .map((x) => (x.name ? x.name + ': ' : '') + this.generate(x.value)) .join(', ')})`; } } @@ -171,20 +162,12 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} @gen(DataModel) private _generateDataModel(ast: DataModel) { - return `${ast.isAbstract ? 'abstract ' : ''}${ - ast.isView ? 'view' : 'model' - } ${ast.name}${ - ast.superTypes.length > 0 - ? ' extends ' + - ast.superTypes.map((x) => x.ref?.name).join(', ') - : '' + return `${ast.isAbstract ? 'abstract ' : ''}${ast.isView ? 'view' : 'model'} ${ast.name}${ + ast.superTypes.length > 0 ? ' extends ' + ast.superTypes.map((x) => x.ref?.name).join(', ') : '' } { ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ ast.attributes.length > 0 - ? '\n\n' + - ast.attributes - .map((x) => this.indent + this.generate(x)) - .join('\n') + ? '\n\n' + ast.attributes.map((x) => this.indent + this.generate(x)).join('\n') : '' } }`; @@ -193,9 +176,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(DataModelField) private _generateDataModelField(ast: DataModelField) { return `${ast.name} ${this.fieldType(ast.type)}${ - ast.attributes.length > 0 - ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') - : '' + ast.attributes.length > 0 ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : '' }`; } @@ -203,11 +184,9 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ const baseType = type.type ? type.type : type.$type == 'DataModelFieldType' && type.unsupported - ? 'Unsupported(' + this.generate(type.unsupported.value) + ')' - : type.reference?.$refText; - return `${baseType}${type.array ? '[]' : ''}${ - type.optional ? '?' : '' - }`; + ? 'Unsupported(' + this.generate(type.unsupported.value) + ')' + : type.reference?.$refText; + return `${baseType}${type.array ? '[]' : ''}${type.optional ? '?' : ''}`; } @gen(DataModelAttribute) @@ -221,9 +200,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ } private attribute(ast: DataModelAttribute | DataModelFieldAttribute) { - const args = ast.args.length - ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` - : ''; + const args = ast.args.length ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` : ''; return `${resolved(ast.decl).name}${args}`; } @@ -238,9 +215,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(ObjectExpr) private _generateObjectExpr(ast: ObjectExpr) { - return `{ ${ast.fields - .map((field) => this.objectField(field)) - .join(', ')} }`; + return `{ ${ast.fields.map((field) => this.objectField(field)).join(', ')} }`; } private objectField(field: FieldInitializer) { @@ -254,9 +229,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(StringLiteral) private _generateLiteralExpr(ast: LiteralExpr) { - return this.options.quote === 'single' - ? `'${ast.value}'` - : `"${ast.value}"`; + return this.options.quote === 'single' ? `'${ast.value}'` : `"${ast.value}"`; } @gen(NumberLiteral) @@ -271,20 +244,16 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(UnaryExpr) private _generateUnaryExpr(ast: UnaryExpr) { - return `${ast.operator}${this.unaryExprSpace}${this.generate( - ast.operand - )}`; + return `${ast.operator}${this.unaryExprSpace}${this.generate(ast.operand)}`; } @gen(BinaryExpr) private _generateBinaryExpr(ast: BinaryExpr) { const operator = ast.operator; - const isCollectionPredicate = - this.isCollectionPredicateOperator(operator); + const isCollectionPredicate = this.isCollectionPredicateOperator(operator); const rightExpr = this.generate(ast.right); - const { left: isLeftParenthesis, right: isRightParenthesis } = - this.isParenthesesNeededForBinaryExpr(ast); + const { left: isLeftParenthesis, right: isRightParenthesis } = this.isParenthesesNeededForBinaryExpr(ast); return `${isLeftParenthesis ? '(' : ''}${this.generate(ast.left)}${ isLeftParenthesis ? ')' : '' @@ -297,9 +266,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(ReferenceExpr) private _generateReferenceExpr(ast: ReferenceExpr) { - const args = ast.args.length - ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` - : ''; + const args = ast.args.length ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` : ''; return `${ast.target.ref?.name}${args}`; } @@ -315,9 +282,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(InvocationExpr) private _generateInvocationExpr(ast: InvocationExpr) { - return `${ast.function.ref?.name}(${ast.args - .map((x) => this.argument(x)) - .join(', ')})`; + return `${ast.function.ref?.name}(${ast.args.map((x) => this.argument(x)).join(', ')})`; } @gen(NullExpr) @@ -332,30 +297,22 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(Attribute) private _generateAttribute(ast: Attribute) { - return `attribute ${ast.name}(${ast.params - .map((x) => this.generate(x)) - .join(', ')})`; + return `attribute ${ast.name}(${ast.params.map((x) => this.generate(x)).join(', ')})`; } @gen(AttributeParam) private _generateAttributeParam(ast: AttributeParam) { - return `${ast.default ? '_ ' : ''}${ast.name}: ${this.generate( - ast.type - )}`; + return `${ast.default ? '_ ' : ''}${ast.name}: ${this.generate(ast.type)}`; } @gen(AttributeParamType) private _generateAttributeParamType(ast: AttributeParamType) { - return `${ast.type ?? ast.reference?.$refText}${ast.array ? '[]' : ''}${ - ast.optional ? '?' : '' - }`; + return `${ast.type ?? ast.reference?.$refText}${ast.array ? '[]' : ''}${ast.optional ? '?' : ''}`; } @gen(FunctionDecl) private _generateFunctionDecl(ast: FunctionDecl) { - return `function ${ast.name}(${ast.params - .map((x) => this.generate(x)) - .join(', ')}) ${ + return `function ${ast.name}(${ast.params.map((x) => this.generate(x)).join(', ')}) ${ ast.returnType ? ': ' + this.generate(ast.returnType) : '' } {}`; } @@ -375,10 +332,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ return `type ${ast.name} { ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ ast.attributes.length > 0 - ? '\n\n' + - ast.attributes - .map((x) => this.indent + this.generate(x)) - .join('\n') + ? '\n\n' + ast.attributes.map((x) => this.indent + this.generate(x)).join('\n') : '' } }`; @@ -387,9 +341,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(TypeDefField) private _generateTypeDefField(ast: TypeDefField) { return `${ast.name} ${this.fieldType(ast.type)}${ - ast.attributes.length > 0 - ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') - : '' + ast.attributes.length > 0 ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : '' }`; } @@ -415,15 +367,13 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ } { const result = { left: false, right: false }; const operator = ast.operator; - const isCollectionPredicate = - this.isCollectionPredicateOperator(operator); + const isCollectionPredicate = this.isCollectionPredicateOperator(operator); const currentPriority = BinaryExprOperatorPriority[operator]; if ( ast.left.$type === BinaryExpr && - BinaryExprOperatorPriority[(ast.left as BinaryExpr)['operator']] < - currentPriority + BinaryExprOperatorPriority[(ast.left as BinaryExpr)['operator']] < currentPriority ) { result.left = true; } @@ -434,8 +384,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ if ( !isCollectionPredicate && ast.right.$type === BinaryExpr && - BinaryExprOperatorPriority[(ast.right as BinaryExpr)['operator']] <= - currentPriority + BinaryExprOperatorPriority[(ast.right as BinaryExpr)['operator']] <= currentPriority ) { result.right = true; } diff --git a/packages/tanstack-query/eslint.config.js b/packages/tanstack-query/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/tanstack-query/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index 7594d0a8..c633ed60 100644 --- a/packages/tanstack-query/package.json +++ b/packages/tanstack-query/package.json @@ -6,7 +6,8 @@ "type": "module", "private": true, "scripts": { - "build": "tsup-node" + "build": "tsup-node", + "lint": "eslint src --ext ts" }, "keywords": [], "author": "", @@ -27,7 +28,8 @@ "@zenstackhq/runtime": "workspace:*" }, "devDependencies": { - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*" }, "peerDependencies": { "@tanstack/react-query": "^5.0.0" diff --git a/packages/tanstack-query/src/react.ts b/packages/tanstack-query/src/react.ts index 3c736345..2b9bb819 100644 --- a/packages/tanstack-query/src/react.ts +++ b/packages/tanstack-query/src/react.ts @@ -5,44 +5,26 @@ import type { UseQueryOptions, UseQueryResult, } from '@tanstack/react-query'; -import type { - CreateArgs, - FindArgs, - ModelResult, - SelectSubset, -} from '@zenstackhq/runtime/client'; +import type { CreateArgs, FindArgs, ModelResult, SelectSubset } from '@zenstackhq/runtime/client'; import type { GetModels, SchemaDef } from '@zenstackhq/runtime/schema'; export type toHooks = { - [Model in GetModels as Uncapitalize]: ToModelHooks< - Schema, - Model - >; + [Model in GetModels as Uncapitalize]: ToModelHooks; }; type ToModelHooks> = { findMany>( args?: SelectSubset>, - options?: Omit< - UseQueryOptions[]>, - 'queryKey' - > + options?: Omit[]>, 'queryKey'>, ): UseQueryResult[]>; findFirst>( args?: SelectSubset>, - options?: Omit< - UseQueryOptions[]>, - 'queryKey' - > + options?: Omit[]>, 'queryKey'>, ): UseQueryResult | null>; create>( - options?: UseMutationOptions< - ModelResult, - DefaultError, - T - > + options?: UseMutationOptions, DefaultError, T>, ): UseMutationResult, DefaultError, T>; }; @@ -50,25 +32,17 @@ function uncapitalize(s: string) { return s.charAt(0).toLowerCase() + s.slice(1); } -export function toHooks( - schema: Schema -): toHooks { +export function toHooks(schema: Schema): toHooks { return Object.entries(schema.models).reduce( (acc, [model, _]) => Object.assign(acc, { - [uncapitalize(model)]: toModelHooks( - schema, - model as GetModels - ), + [uncapitalize(model)]: toModelHooks(schema, model as GetModels), }), - {} as toHooks + {} as toHooks, ); } -function toModelHooks< - Schema extends SchemaDef, - Model extends GetModels ->(schema: Schema, model: Model): any { +function toModelHooks>(schema: Schema, model: Model): any { const modelDef = schema.models[model]; if (!modelDef) { throw new Error(`Model ${model} not found in schema`); diff --git a/packages/testtools/eslint.config.js b/packages/testtools/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/testtools/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/testtools/package.json b/packages/testtools/package.json index fe10770b..f810eaea 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@types/tmp": "^0.2.6", - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*" } } diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 5a134293..8214c4ef 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -31,21 +31,16 @@ datasource db { export async function generateTsSchema( schemaText: string, provider: 'sqlite' | 'postgresql' = 'sqlite', - dbName?: string + dbName?: string, ) { const { name: workDir } = tmp.dirSync({ unsafeCleanup: true }); console.log(`Working directory: ${workDir}`); const zmodelPath = path.join(workDir, 'schema.zmodel'); const noPrelude = schemaText.includes('datasource '); - fs.writeFileSync( - zmodelPath, - `${noPrelude ? '' : makePrelude(provider, dbName)}\n\n${schemaText}` - ); + fs.writeFileSync(zmodelPath, `${noPrelude ? '' : makePrelude(provider, dbName)}\n\n${schemaText}`); - const pluginModelFiles = glob.sync( - path.resolve(__dirname, '../../runtime/src/plugins/**/plugin.zmodel') - ); + const pluginModelFiles = glob.sync(path.resolve(__dirname, '../../runtime/src/plugins/**/plugin.zmodel')); const generator = new TsSchemaGenerator(); const tsPath = path.join(workDir, 'schema.ts'); @@ -62,7 +57,7 @@ export async function generateTsSchema( fs.symlinkSync( path.join(__dirname, '../node_modules', entry), path.join(workDir, 'node_modules', entry), - 'dir' + 'dir', ); } @@ -73,7 +68,7 @@ export async function generateTsSchema( fs.symlinkSync( path.join(__dirname, `../../${pkg}/dist`), path.join(workDir, `node_modules/@zenstackhq/${pkg}`), - 'dir' + 'dir', ); } @@ -83,7 +78,7 @@ export async function generateTsSchema( name: 'test', version: '1.0.0', type: 'module', - }) + }), ); fs.writeFileSync( @@ -96,7 +91,7 @@ export async function generateTsSchema( esModuleInterop: true, skipLibCheck: true, }, - }) + }), ); // compile the generated TS schema diff --git a/packages/zod/eslint.config.js b/packages/zod/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/zod/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/zod/package.json b/packages/zod/package.json index 4d9b2503..8d6c3879 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -6,7 +6,8 @@ "main": "index.js", "private": true, "scripts": { - "build": "tsup-node" + "build": "tsup-node", + "lint": "eslint src --ext ts" }, "keywords": [], "author": "", @@ -28,7 +29,8 @@ "ts-pattern": "catalog:" }, "devDependencies": { - "@zenstackhq/typescript-config": "workspace:*" + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*" }, "peerDependencies": { "zod": "catalog:" diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index 978da782..4ca7ade0 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -1,33 +1,21 @@ -import type { - FieldDef, - GetModels, - SchemaDef, -} from '@zenstackhq/runtime/schema'; +import type { FieldDef, GetModels, SchemaDef } from '@zenstackhq/runtime/schema'; import { match, P } from 'ts-pattern'; import { z, ZodType } from 'zod/v4'; import type { SelectSchema } from './types'; -export function makeSelectSchema< - Schema extends SchemaDef, - Model extends GetModels ->(schema: Schema, model: Model) { - return z.object(mapFields(schema, model)) as SelectSchema< - Schema, - typeof model - >; +export function makeSelectSchema>( + schema: Schema, + model: Model, +) { + return z.object(mapFields(schema, model)) as SelectSchema; } -function mapFields( - schema: Schema, - model: GetModels -): any { +function mapFields(schema: Schema, model: GetModels): any { const modelDef = schema.models[model]; if (!modelDef) { throw new Error(`Model ${model} not found in schema`); } - const scalarFields = Object.entries(modelDef.fields).filter( - ([_, fieldDef]) => !fieldDef.relation - ); + const scalarFields = Object.entries(modelDef.fields).filter(([_, fieldDef]) => !fieldDef.relation); const result: Record = {}; for (const [field, fieldDef] of scalarFields) { result[field] = makeScalarSchema(fieldDef); diff --git a/packages/zod/src/types.ts b/packages/zod/src/types.ts index 57e02e94..389178e7 100644 --- a/packages/zod/src/types.ts +++ b/packages/zod/src/types.ts @@ -1,21 +1,7 @@ -import type { - FieldType, - GetModels, - ScalarFields, - SchemaDef, -} from '@zenstackhq/runtime/schema'; -import type { - ZodBoolean, - ZodNumber, - ZodObject, - ZodString, - ZodUnknown, -} from 'zod/v4'; +import type { FieldType, GetModels, ScalarFields, SchemaDef } from '@zenstackhq/runtime/schema'; +import type { ZodBoolean, ZodNumber, ZodObject, ZodString, ZodUnknown } from 'zod/v4'; -export type SelectSchema< - Schema extends SchemaDef, - Model extends GetModels -> = ZodObject<{ +export type SelectSchema> = ZodObject<{ [Key in ScalarFields]: MapScalarType; }>; @@ -23,19 +9,19 @@ type MapScalarType< Schema extends SchemaDef, Model extends GetModels, Field extends ScalarFields, - Type = FieldType + Type = FieldType, > = Type extends 'String' ? ZodString : Type extends 'Int' - ? ZodNumber - : Type extends 'BigInt' - ? ZodNumber - : Type extends 'Float' - ? ZodNumber - : Type extends 'Decimal' - ? ZodNumber - : Type extends 'DateTime' - ? ZodString - : Type extends 'Boolean' - ? ZodBoolean - : ZodUnknown; + ? ZodNumber + : Type extends 'BigInt' + ? ZodNumber + : Type extends 'Float' + ? ZodNumber + : Type extends 'Decimal' + ? ZodNumber + : Type extends 'DateTime' + ? ZodString + : Type extends 'Boolean' + ? ZodBoolean + : ZodUnknown; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32441e47..7ccaec7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,24 +29,24 @@ importers: .: devDependencies: + '@eslint/js': + specifier: ^9.29.0 + version: 9.29.0 '@swc/core': specifier: ^1.12.5 version: 1.12.5 '@types/node': specifier: ^20.17.24 version: 20.17.24 - '@typescript-eslint/eslint-plugin': - specifier: ~7.3.1 - version: 7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/parser': - specifier: ~7.3.1 - version: 7.3.1(eslint@8.57.1)(typescript@5.8.3) eslint: - specifier: ~8.57.1 - version: 8.57.1 + specifier: ~9.29.0 + version: 9.29.0(jiti@2.4.2) npm-run-all: specifier: ^4.1.5 version: 4.1.5 + prettier: + specifier: ^3.5.3 + version: 3.5.3 tsup: specifier: ^8.5.0 version: 8.5.0(@swc/core@1.12.5)(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3) @@ -59,6 +59,9 @@ importers: typescript: specifier: 'catalog:' version: 5.8.3 + typescript-eslint: + specifier: ^8.34.1 + version: 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@20.17.24)(jiti@2.4.2)(tsx@4.20.3) @@ -111,6 +114,9 @@ importers: '@types/tmp': specifier: ^0.2.6 version: 0.2.6 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config '@zenstackhq/runtime': specifier: workspace:* version: link:../runtime @@ -139,10 +145,15 @@ importers: specifier: ^5.4.1 version: 5.4.1 devDependencies: + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config + packages/eslint-config: {} + packages/ide/vscode: dependencies: '@zenstackhq/language': @@ -161,6 +172,9 @@ importers: '@types/vscode': specifier: ^1.63.0 version: 1.101.0 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../../eslint-config '@zenstackhq/typescript-config': specifier: workspace:* version: link:../../typescript-config @@ -180,6 +194,9 @@ importers: '@types/pluralize': specifier: ^0.0.33 version: 0.0.33 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -241,6 +258,9 @@ importers: '@types/tmp': specifier: ^0.2.6 version: 0.2.6 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config '@zenstackhq/language': specifier: workspace:* version: link:../language @@ -284,6 +304,9 @@ importers: '@types/tmp': specifier: ^0.2.6 version: 0.2.6 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -303,6 +326,9 @@ importers: specifier: workspace:* version: link:../runtime devDependencies: + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -340,6 +366,9 @@ importers: '@types/tmp': specifier: ^0.2.6 version: 0.2.6 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -358,6 +387,9 @@ importers: specifier: 'catalog:' version: 3.25.67 devDependencies: + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -705,26 +737,57 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-array@0.20.1': + resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-helpers@0.2.3': + resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.0': + resolution: {integrity: sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.29.0': + resolution: {integrity: sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.2': + resolution: {integrity: sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -1023,75 +1086,70 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - '@types/semver@7.7.0': - resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} - '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} '@types/vscode@1.101.0': resolution: {integrity: sha512-ZWf0IWa+NGegdW3iU42AcDTFHWW7fApLdkdnBqwYEtHVIBGbTu0ZNQKP/kX3Ds/uMJXIMQNAojHR4vexCEEz5Q==} - '@typescript-eslint/eslint-plugin@7.3.1': - resolution: {integrity: sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/eslint-plugin@8.34.1': + resolution: {integrity: sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser': ^8.34.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@7.3.1': - resolution: {integrity: sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/parser@8.34.1': + resolution: {integrity: sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@7.3.1': - resolution: {integrity: sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==} - engines: {node: ^18.18.0 || >=20.0.0} + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@7.3.1': - resolution: {integrity: sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/project-service@8.34.1': + resolution: {integrity: sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@7.3.1': - resolution: {integrity: sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.34.1': + resolution: {integrity: sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@7.3.1': - resolution: {integrity: sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/tsconfig-utils@8.34.1': + resolution: {integrity: sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@7.3.1': - resolution: {integrity: sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/type-utils@8.34.1': + resolution: {integrity: sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@7.3.1': - resolution: {integrity: sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.34.1': + resolution: {integrity: sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@typescript-eslint/typescript-estree@8.34.1': + resolution: {integrity: sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.34.1': + resolution: {integrity: sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.34.1': + resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -1165,10 +1223,6 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - arraybuffer.prototype.slice@1.0.4: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} @@ -1394,14 +1448,6 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1471,23 +1517,31 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.29.0: + resolution: {integrity: sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} @@ -1540,9 +1594,9 @@ packages: picomatch: optional: true - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -1558,9 +1612,9 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -1584,9 +1638,6 @@ packages: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1640,22 +1691,14 @@ packages: engines: {node: 20 || >=22} hasBin: true - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1707,6 +1750,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1715,10 +1762,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1804,10 +1847,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -2011,10 +2050,6 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -2133,10 +2168,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@2.0.1: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} engines: {node: '>=4'} @@ -2160,10 +2191,6 @@ packages: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2314,6 +2341,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + engines: {node: '>=14'} + hasBin: true + prisma@6.5.0: resolution: {integrity: sha512-yUGXmWqv5F4PByMSNbYFxke/WbnyTLjnJ5bKr8fLkcnY7U5rU9rUTh/+Fja+gOrRxEgtCbCtca94IeITj4j/pg==} engines: {node: '>=18.18'} @@ -2399,11 +2431,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rollup@4.44.0: resolution: {integrity: sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2505,10 +2532,6 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2617,9 +2640,6 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2667,11 +2687,11 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} peerDependencies: - typescript: '>=4.2.0' + typescript: '>=4.8.4' ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -2749,10 +2769,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -2769,6 +2785,13 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typescript-eslint@8.34.1: + resolution: {integrity: sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -3137,19 +3160,37 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@2.4.2))': dependencies: - eslint: 8.57.1 + eslint: 9.29.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/eslintrc@2.1.4': + '@eslint/config-array@0.20.1': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.2.3': {} + + '@eslint/core@0.14.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.15.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 debug: 4.4.1 - espree: 9.6.1 - globals: 13.24.0 + espree: 10.4.0 + globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.0 @@ -3158,19 +3199,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} + '@eslint/js@9.29.0': {} - '@humanwhocodes/config-array@0.13.0': + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.2': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@eslint/core': 0.15.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} '@isaacs/cliui@8.0.2': dependencies: @@ -3425,99 +3474,101 @@ snapshots: '@types/semver@7.5.8': {} - '@types/semver@7.7.0': {} - '@types/tmp@0.2.6': {} '@types/vscode@1.101.0': {} - '@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.3.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.3.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 7.3.1 - '@typescript-eslint/type-utils': 7.3.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/utils': 7.3.1(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 7.3.1 - debug: 4.4.1 - eslint: 8.57.1 + '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/type-utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.34.1 + eslint: 9.29.0(jiti@2.4.2) graphemer: 1.4.0 - ignore: 5.3.2 + ignore: 7.0.5 natural-compare: 1.4.0 - semver: 7.7.2 - ts-api-utils: 1.4.3(typescript@5.8.3) - optionalDependencies: + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.3.1(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 7.3.1 - '@typescript-eslint/types': 7.3.1 - '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 7.3.1 + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.34.1 + debug: 4.4.1 + eslint: 9.29.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.34.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3) + '@typescript-eslint/types': 8.34.1 debug: 4.4.1 - eslint: 8.57.1 - optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.3.1': + '@typescript-eslint/scope-manager@8.34.1': + dependencies: + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/visitor-keys': 8.34.1 + + '@typescript-eslint/tsconfig-utils@8.34.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 7.3.1 - '@typescript-eslint/visitor-keys': 7.3.1 + typescript: 5.8.3 - '@typescript-eslint/type-utils@7.3.1(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.8.3) - '@typescript-eslint/utils': 7.3.1(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.1 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.8.3) - optionalDependencies: + eslint: 9.29.0(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@7.3.1': {} + '@typescript-eslint/types@8.34.1': {} - '@typescript-eslint/typescript-estree@7.3.1(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.34.1(typescript@5.8.3)': dependencies: - '@typescript-eslint/types': 7.3.1 - '@typescript-eslint/visitor-keys': 7.3.1 + '@typescript-eslint/project-service': 8.34.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3) + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/visitor-keys': 8.34.1 debug: 4.4.1 - globby: 11.1.0 + fast-glob: 3.3.3 is-glob: 4.0.3 - minimatch: 9.0.3 + minimatch: 9.0.5 semver: 7.7.2 - ts-api-utils: 1.4.3(typescript@5.8.3) - optionalDependencies: + ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.3.1(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/utils@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.7.0 - '@typescript-eslint/scope-manager': 7.3.1 - '@typescript-eslint/types': 7.3.1 - '@typescript-eslint/typescript-estree': 7.3.1(typescript@5.8.3) - eslint: 8.57.1 - semver: 7.7.2 + '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.34.1 + '@typescript-eslint/types': 8.34.1 + '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3) + eslint: 9.29.0(jiti@2.4.2) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@7.3.1': + '@typescript-eslint/visitor-keys@8.34.1': dependencies: - '@typescript-eslint/types': 7.3.1 - eslint-visitor-keys: 3.4.3 - - '@ungap/structured-clone@1.3.0': {} + '@typescript-eslint/types': 8.34.1 + eslint-visitor-keys: 4.2.1 '@vitest/expect@3.2.4': dependencies: @@ -3597,8 +3648,6 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-union@2.1.0: {} - arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 @@ -3829,14 +3878,6 @@ snapshots: detect-libc@2.0.3: {} - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4003,61 +4044,62 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-scope@7.2.2: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint@8.57.1: + eslint-visitor-keys@4.2.1: {} + + eslint@9.29.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 + '@eslint/config-array': 0.20.1 + '@eslint/config-helpers': 0.2.3 + '@eslint/core': 0.14.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.29.0 + '@eslint/plugin-kit': 0.3.2 + '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.1 - doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 + optionalDependencies: + jiti: 2.4.2 transitivePeerDependencies: - supports-color - espree@9.6.1: + espree@10.4.0: dependencies: acorn: 8.15.0 acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 + eslint-visitor-keys: 4.2.1 esquery@1.6.0: dependencies: @@ -4101,9 +4143,9 @@ snapshots: optionalDependencies: picomatch: 4.0.2 - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 file-uri-to-path@1.0.0: {} @@ -4122,11 +4164,10 @@ snapshots: mlly: 1.7.4 rollup: 4.44.0 - flat-cache@3.2.0: + flat-cache@4.0.1: dependencies: flatted: 3.3.3 keyv: 4.5.4 - rimraf: 3.0.2 flatted@3.3.3: {} @@ -4152,8 +4193,6 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true @@ -4230,33 +4269,13 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.0 - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 + globals@14.0.0: {} globalthis@1.0.4: dependencies: define-properties: 1.2.1 gopd: 1.2.0 - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -4293,6 +4312,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4300,11 +4321,6 @@ snapshots: imurmurhash@0.1.4: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - inherits@2.0.4: {} ini@1.3.8: {} @@ -4389,8 +4405,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-plain-object@5.0.0: {} is-regex@1.2.1: @@ -4585,10 +4599,6 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimatch@9.0.3: - dependencies: - brace-expansion: 2.0.2 - minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -4722,8 +4732,6 @@ snapshots: path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@2.0.1: {} path-key@3.1.1: {} @@ -4744,8 +4752,6 @@ snapshots: dependencies: pify: 3.0.0 - path-type@4.0.0: {} - pathe@2.0.3: {} pathval@2.0.0: {} @@ -4872,6 +4878,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.5.3: {} + prisma@6.5.0(typescript@5.8.3): dependencies: '@prisma/config': 6.5.0 @@ -4962,10 +4970,6 @@ snapshots: reusify@1.1.0: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rollup@4.44.0: dependencies: '@types/estree': 1.0.8 @@ -5101,8 +5105,6 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 - slash@3.0.0: {} - source-map-js@1.2.1: {} source-map@0.8.0-beta.0: @@ -5233,8 +5235,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - text-table@0.2.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -5272,7 +5272,7 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@1.4.3(typescript@5.8.3): + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -5358,8 +5358,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.20.2: {} - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -5393,6 +5391,16 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript-eslint@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.29.0(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + typescript@5.8.3: {} ufo@1.6.1: {} diff --git a/samples/blog/README.md b/samples/blog/README.md index 8b351a5f..cdc6a49c 100644 --- a/samples/blog/README.md +++ b/samples/blog/README.md @@ -2,23 +2,23 @@ ## Prerequisites -- Clone the repo -- `pnpm install` from the root -- `pnpm build` from the root +- Clone the repo +- `pnpm install` from the root +- `pnpm build` from the root ## Running the sample -- `cd samples/blog` -- `pnpm generate` -- `pnpm db:migrate` -- `pnpm dev` +- `cd samples/blog` +- `pnpm generate` +- `pnpm db:migrate` +- `pnpm dev` ## Overview -- ZModel is located in [zenstack/schema.zmodel](./zenstack/schema.zmodel). -- When you run `zenstack generate`, a TypeScript version of the schema is generated to [zenstack/schema.ts](./zenstack/schema.ts). -- A Prisma schema [zenstack/schema.prisma](./zenstack/schema.prisma) is also generated. It's used for generating and running database migrations, and you can also use it for other purposes as needed. -- You can create a database client with the TypeScript schema like: +- ZModel is located in [zenstack/schema.zmodel](./zenstack/schema.zmodel). +- When you run `zenstack generate`, a TypeScript version of the schema is generated to [zenstack/schema.ts](./zenstack/schema.ts). +- A Prisma schema [zenstack/schema.prisma](./zenstack/schema.prisma) is also generated. It's used for generating and running database migrations, and you can also use it for other purposes as needed. +- You can create a database client with the TypeScript schema like: ```ts import { ZenStackClient } from '@zenstackhq/runtime'; import { schema } from './zenstack/schema'; @@ -27,9 +27,9 @@ dialectConfig: { database: new SQLite('./zenstack/dev.db') }, }); ``` -- Run `zenstack migrate dev` to generate and apply database migrations. It internally calls `prisma migrate dev`. Same for `zenstack migrate deploy`. -- ZenStack v3 doesn't generate into "node_modules" anymore. The generated TypeScript schema file can be checked in to source control, and you decide how to build or bundle it with your application. -- The TS schema will also serve as the foundation of inferring types of other artifacts, e.g., zod schemas, frontend hooks, etc. +- Run `zenstack migrate dev` to generate and apply database migrations. It internally calls `prisma migrate dev`. Same for `zenstack migrate deploy`. +- ZenStack v3 doesn't generate into "node_modules" anymore. The generated TypeScript schema file can be checked in to source control, and you decide how to build or bundle it with your application. +- The TS schema will also serve as the foundation of inferring types of other artifacts, e.g., zod schemas, frontend hooks, etc. ## Features @@ -39,7 +39,7 @@ Replicating PrismaClient's CRUD API is around 80% done, including typing and run Not supported yet: -- `$extends` +- `$extends` ### 2. Using Kysely expression builder to express complex queries in `where` @@ -81,10 +81,7 @@ const db = createClient({ User: { emailDomain: (eb) => // build SQL expression: substr(email, instr(email, '@') + 1) - eb.fn('substr', [ - eb.ref('email'), - eb(eb.fn('instr', [eb.ref('email'), eb.val('@')]), '+', 1), - ]), + eb.fn('substr', [eb.ref('email'), eb(eb.fn('instr', [eb.ref('email'), eb.val('@')]), '+', 1)]), }, }, }); diff --git a/samples/blog/main.ts b/samples/blog/main.ts index 28675e18..ad3a0c34 100644 --- a/samples/blog/main.ts +++ b/samples/blog/main.ts @@ -13,9 +13,7 @@ async function main() { eb .selectFrom('Post') .whereRef('Post.authorId', '=', 'User.id') - .select(({ fn }) => - fn.countAll().as('postCount') - ), + .select(({ fn }) => fn.countAll().as('postCount')), }, }, }).$use({ @@ -23,9 +21,7 @@ async function main() { async onQuery({ model, operation, proceed, queryArgs }) { const start = Date.now(); const result = await proceed(queryArgs); - console.log( - `[cost] ${model} ${operation} took ${Date.now() - start}ms` - ); + console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); return result; }, }); diff --git a/samples/blog/zenstack/schema.ts b/samples/blog/zenstack/schema.ts index a73b7230..18211550 100644 --- a/samples/blog/zenstack/schema.ts +++ b/samples/blog/zenstack/schema.ts @@ -3,6 +3,8 @@ // This file is automatically generated by ZenStack CLI and should not be manually updated. // ////////////////////////////////////////////////////////////////////////////////////////////// +/* eslint-disable */ + import { type SchemaDef, type OperandExpression, ExpressionUtils } from "@zenstackhq/runtime/schema"; export const schema = { provider: {