From 5d1053f6580c0296a28fb3073d43aabca3684eef Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:25:23 +0800 Subject: [PATCH 1/8] fix(schema): use type-only imports to prevent bundling server dependencies in client code (#668) * Initial plan * Fix: Change ModelResult/TypeDefResult imports to type-only imports in ts-schema-generator Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> * Regenerate test schemas with type-only imports Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> * Fix: Update bun runtime models.ts to use type-only imports Co-authored-by: Yiming Cao --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Yiming Cao --- packages/sdk/src/ts-schema-generator.ts | 6 +++--- samples/orm/zenstack/models.ts | 2 +- tests/e2e/apps/rally/zenstack/models.ts | 2 +- tests/e2e/github-repos/cal.com/models.ts | 2 +- tests/e2e/github-repos/formbricks/models.ts | 2 +- tests/e2e/github-repos/trigger.dev/models.ts | 2 +- tests/e2e/orm/plugin-infra/ext-query-args/models.ts | 2 +- tests/e2e/orm/schemas/auth-type/models.ts | 2 +- tests/e2e/orm/schemas/basic/models.ts | 2 +- tests/e2e/orm/schemas/default-auth/models.ts | 2 +- tests/e2e/orm/schemas/delegate/models.ts | 2 +- tests/e2e/orm/schemas/json/models.ts | 2 +- tests/e2e/orm/schemas/name-mapping/models.ts | 2 +- tests/e2e/orm/schemas/omit/models.ts | 2 +- tests/e2e/orm/schemas/petstore/models.ts | 2 +- tests/e2e/orm/schemas/procedures/models.ts | 2 +- tests/e2e/orm/schemas/todo/models.ts | 2 +- tests/e2e/orm/schemas/typed-json/models.ts | 2 +- tests/e2e/orm/schemas/typing/models.ts | 2 +- tests/regression/test/issue-204/models.ts | 2 +- tests/regression/test/issue-422/models.ts | 2 +- tests/regression/test/issue-503/models.ts | 2 +- tests/runtimes/bun/schemas/models.ts | 2 +- tests/runtimes/edge-runtime/schemas/models.ts | 2 +- 24 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 413131269..ac6fcf00f 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -1359,18 +1359,18 @@ export class TsSchemaGenerator { ts.factory.createImportDeclaration( undefined, ts.factory.createImportClause( - false, + true, undefined, ts.factory.createNamedImports([ ts.factory.createImportSpecifier( - true, + false, undefined, ts.factory.createIdentifier(`ModelResult as $ModelResult`), ), ...(model.declarations.some(isTypeDef) ? [ ts.factory.createImportSpecifier( - true, + false, undefined, ts.factory.createIdentifier(`TypeDefResult as $TypeDefResult`), ), diff --git a/samples/orm/zenstack/models.ts b/samples/orm/zenstack/models.ts index e2db380ea..b97ba05f6 100644 --- a/samples/orm/zenstack/models.ts +++ b/samples/orm/zenstack/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; /** * User model */ diff --git a/tests/e2e/apps/rally/zenstack/models.ts b/tests/e2e/apps/rally/zenstack/models.ts index e30e340b6..06e6ac4ff 100644 --- a/tests/e2e/apps/rally/zenstack/models.ts +++ b/tests/e2e/apps/rally/zenstack/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type Account = $ModelResult<$Schema, "Account">; export type User = $ModelResult<$Schema, "User">; export type VerificationToken = $ModelResult<$Schema, "VerificationToken">; diff --git a/tests/e2e/github-repos/cal.com/models.ts b/tests/e2e/github-repos/cal.com/models.ts index 26c9b777e..c8f35c09f 100644 --- a/tests/e2e/github-repos/cal.com/models.ts +++ b/tests/e2e/github-repos/cal.com/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type Host = $ModelResult<$Schema, "Host">; export type CalVideoSettings = $ModelResult<$Schema, "CalVideoSettings">; export type EventType = $ModelResult<$Schema, "EventType">; diff --git a/tests/e2e/github-repos/formbricks/models.ts b/tests/e2e/github-repos/formbricks/models.ts index 4c57997cf..740a0e041 100644 --- a/tests/e2e/github-repos/formbricks/models.ts +++ b/tests/e2e/github-repos/formbricks/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; /** * Represents a webhook endpoint for receiving survey-related events. * Webhooks can be configured to receive notifications about response creation, updates, and completion. diff --git a/tests/e2e/github-repos/trigger.dev/models.ts b/tests/e2e/github-repos/trigger.dev/models.ts index ba6e7cd16..70ee88aae 100644 --- a/tests/e2e/github-repos/trigger.dev/models.ts +++ b/tests/e2e/github-repos/trigger.dev/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type InvitationCode = $ModelResult<$Schema, "InvitationCode">; /** diff --git a/tests/e2e/orm/plugin-infra/ext-query-args/models.ts b/tests/e2e/orm/plugin-infra/ext-query-args/models.ts index 7a605bdbc..996d95e2a 100644 --- a/tests/e2e/orm/plugin-infra/ext-query-args/models.ts +++ b/tests/e2e/orm/plugin-infra/ext-query-args/models.ts @@ -6,5 +6,5 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; diff --git a/tests/e2e/orm/schemas/auth-type/models.ts b/tests/e2e/orm/schemas/auth-type/models.ts index 75911fc76..2d78f9bbd 100644 --- a/tests/e2e/orm/schemas/auth-type/models.ts +++ b/tests/e2e/orm/schemas/auth-type/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; export type Foo = $ModelResult<$Schema, "Foo">; export type Permission = $TypeDefResult<$Schema, "Permission">; export type Auth = $TypeDefResult<$Schema, "Auth">; diff --git a/tests/e2e/orm/schemas/basic/models.ts b/tests/e2e/orm/schemas/basic/models.ts index 733e7df68..39bd52fdf 100644 --- a/tests/e2e/orm/schemas/basic/models.ts +++ b/tests/e2e/orm/schemas/basic/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; export type Comment = $ModelResult<$Schema, "Comment">; diff --git a/tests/e2e/orm/schemas/default-auth/models.ts b/tests/e2e/orm/schemas/default-auth/models.ts index 3f7f08e6b..624cfb8ec 100644 --- a/tests/e2e/orm/schemas/default-auth/models.ts +++ b/tests/e2e/orm/schemas/default-auth/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Profile = $ModelResult<$Schema, "Profile">; export type Address = $ModelResult<$Schema, "Address">; diff --git a/tests/e2e/orm/schemas/delegate/models.ts b/tests/e2e/orm/schemas/delegate/models.ts index 0a4350d20..e1fe994bd 100644 --- a/tests/e2e/orm/schemas/delegate/models.ts +++ b/tests/e2e/orm/schemas/delegate/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Comment = $ModelResult<$Schema, "Comment">; export type Asset = $ModelResult<$Schema, "Asset">; diff --git a/tests/e2e/orm/schemas/json/models.ts b/tests/e2e/orm/schemas/json/models.ts index 24ea716d8..bdbfabad1 100644 --- a/tests/e2e/orm/schemas/json/models.ts +++ b/tests/e2e/orm/schemas/json/models.ts @@ -6,5 +6,5 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type Foo = $ModelResult<$Schema, "Foo">; diff --git a/tests/e2e/orm/schemas/name-mapping/models.ts b/tests/e2e/orm/schemas/name-mapping/models.ts index 944ad9cb5..9c5a74865 100644 --- a/tests/e2e/orm/schemas/name-mapping/models.ts +++ b/tests/e2e/orm/schemas/name-mapping/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; export const Role = $schema.enums.Role.values; diff --git a/tests/e2e/orm/schemas/omit/models.ts b/tests/e2e/orm/schemas/omit/models.ts index 6636b4d54..5044cb91a 100644 --- a/tests/e2e/orm/schemas/omit/models.ts +++ b/tests/e2e/orm/schemas/omit/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; export type Base = $ModelResult<$Schema, "Base">; diff --git a/tests/e2e/orm/schemas/petstore/models.ts b/tests/e2e/orm/schemas/petstore/models.ts index dfa5b23e6..b55674cb0 100644 --- a/tests/e2e/orm/schemas/petstore/models.ts +++ b/tests/e2e/orm/schemas/petstore/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Pet = $ModelResult<$Schema, "Pet">; export type Order = $ModelResult<$Schema, "Order">; diff --git a/tests/e2e/orm/schemas/procedures/models.ts b/tests/e2e/orm/schemas/procedures/models.ts index 9920c1011..f5d8f42dc 100644 --- a/tests/e2e/orm/schemas/procedures/models.ts +++ b/tests/e2e/orm/schemas/procedures/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Overview = $TypeDefResult<$Schema, "Overview">; export const Role = $schema.enums.Role.values; diff --git a/tests/e2e/orm/schemas/todo/models.ts b/tests/e2e/orm/schemas/todo/models.ts index 635b68dee..7d3c2008e 100644 --- a/tests/e2e/orm/schemas/todo/models.ts +++ b/tests/e2e/orm/schemas/todo/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type Space = $ModelResult<$Schema, "Space">; export type SpaceUser = $ModelResult<$Schema, "SpaceUser">; export type User = $ModelResult<$Schema, "User">; diff --git a/tests/e2e/orm/schemas/typed-json/models.ts b/tests/e2e/orm/schemas/typed-json/models.ts index 2b2474faa..e46ea2c7b 100644 --- a/tests/e2e/orm/schemas/typed-json/models.ts +++ b/tests/e2e/orm/schemas/typed-json/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Profile = $TypeDefResult<$Schema, "Profile">; export type Address = $TypeDefResult<$Schema, "Address">; diff --git a/tests/e2e/orm/schemas/typing/models.ts b/tests/e2e/orm/schemas/typing/models.ts index 15eae9a94..20e775f4e 100644 --- a/tests/e2e/orm/schemas/typing/models.ts +++ b/tests/e2e/orm/schemas/typing/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; export type Profile = $ModelResult<$Schema, "Profile">; diff --git a/tests/regression/test/issue-204/models.ts b/tests/regression/test/issue-204/models.ts index 633c334b2..4a284d844 100644 --- a/tests/regression/test/issue-204/models.ts +++ b/tests/regression/test/issue-204/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { schema as $schema, type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult, type TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm"; export type Foo = $ModelResult<$Schema, "Foo">; export type Configuration = $TypeDefResult<$Schema, "Configuration">; export const ShirtColor = $schema.enums.ShirtColor.values; diff --git a/tests/regression/test/issue-422/models.ts b/tests/regression/test/issue-422/models.ts index 787d1c77c..4255e3959 100644 --- a/tests/regression/test/issue-422/models.ts +++ b/tests/regression/test/issue-422/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type Session = $ModelResult<$Schema, "Session">; export type User = $ModelResult<$Schema, "User">; export type Profile = $ModelResult<$Schema, "Profile">; diff --git a/tests/regression/test/issue-503/models.ts b/tests/regression/test/issue-503/models.ts index e71f3a616..213a31f3a 100644 --- a/tests/regression/test/issue-503/models.ts +++ b/tests/regression/test/issue-503/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type InternalChat = $ModelResult<$Schema, "InternalChat">; export type Message = $ModelResult<$Schema, "Message">; export type Media = $ModelResult<$Schema, "Media">; diff --git a/tests/runtimes/bun/schemas/models.ts b/tests/runtimes/bun/schemas/models.ts index 72654e587..03524da52 100644 --- a/tests/runtimes/bun/schemas/models.ts +++ b/tests/runtimes/bun/schemas/models.ts @@ -6,6 +6,6 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; diff --git a/tests/runtimes/edge-runtime/schemas/models.ts b/tests/runtimes/edge-runtime/schemas/models.ts index 72654e587..03524da52 100644 --- a/tests/runtimes/edge-runtime/schemas/models.ts +++ b/tests/runtimes/edge-runtime/schemas/models.ts @@ -6,6 +6,6 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; From e7a35883bc9eff56b4265f411985c104fe3f58ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n=20Benj=C3=A1min?= Date: Sun, 8 Feb 2026 03:02:26 +0100 Subject: [PATCH 2/8] feat: db pull implementation (#268) * feat: initial db pull implementation * fix: generate imports and attributes for zmodel-code-generator * fix: add option to not exclude imports in loadDocument * fix: continue work on db pull * fix: missing import * fix: rewrite model generation generate model from ground up and diff later * feat: add ast factory * fix: ast factory import order * fix: some runtime bugs * fix: lint fix * fix: update zmodel code generator - include imports in output - fix indentaions - include comments in output * feat: add exclude schemas option * feat: implement initial diff update * fix: update format in zmodel code generator * fix: typo * feat: progress on database introspection and syncing * fix: make ignore behave it does in prisma with no index models * fix: lint fix * feat: make all format options configurable * fix: lint fix * feat: Handle the database type mapping * fix: catch up with feature updates - improve code styling - enable schema support for db pull * fix: add sqlite e2e test and fix some bugs * fix: lint fix * fix: formatting for e2e test schemas * test: run db pull e2e test also for postgres * fix: postgres instorspection schema filter * test: update cli tests * feat(cli): Improves database introspection and syncing Enhances the `db pull` command with a spinner for better UX. Adds color-coded logging to highlight important steps. Provides more detailed output on schema changes, including deleted models, enums, added fields, and deleted attributes. Also includes minor improvements to enum mapping and constraint handling. * fix(cli): fixes field casing and sort issues * chore(cli): remove temporary test script Deletes an unused script used for experimenting with URI path resolution. Cleans up the codebase by removing development-only artifacts. * chore: update pnpm-lock.yaml * feat(cli): add MySQL support for schema introspection Introduces a MySQL-specific introspection provider to support pulling existing database schemas into ZenStack. The implementation includes logic for mapping MySQL data types to ZenStack types, handling auto-incrementing fields, and parsing MySQL-specific enum definitions. It utilizes dynamic imports for database drivers to minimize the CLI footprint for users not targeting MySQL. * fix(cli): improve field matching logic during db pull * feat(cli): enhance SQLite introspection with autoincrement support * fix(cli): refine attribute generation in db pull * test(cli): update db pull tests for SQLite specific behavior * refactor(language): export ZModelServices type * fix(cli): improve sqlite introspection for autoincrement and fk names * feat(cli): enhance field matching logic during pull by using relation fields * refactor(cli): refine relation name generation and table syncing * test(cli): update pull tests to reflect improved schema generation * test(cli): add MySQL support to test utility helpers Extends the testing infrastructure to support MySQL databases. Adds MySQL configuration defaults and environment variable overrides. Updates the prelude generation logic to handle MySQL connection strings and provider types, enabling broader database integration testing across the CLI. * fix(cli): omit default constraint names in table sync Avoids explicitly declaring unique constraint names when they match the default database naming convention. This results in cleaner generated schema code by removing redundant mapping arguments. * fix: correctly handle default values for 'text' type in PostgreSQL * fix: sort table indexes to ensure stable schema generation * refactor: dynamically determine supported db providers in CLI * test: fix typo in pull test description * chore(cli): remove debug artifacts and silence test logs Removes hardcoded file system path debugging and unnecessary console logging from the introspector and test suites. Silences CLI command output during tests to provide a cleaner test execution environment. * fix(cli): ensure MySQL column and index ordering Wraps JSON_ARRAYAGG calls in subqueries with explicit ORDER BY clauses to maintain correct metadata ordering. This addresses a limitation in MySQL versions prior to 8.0.21, where ORDER BY is not supported directly within the JSON_ARRAYAGG function, ensuring consistent introspection results across different database versions. * fix(cli): preserve column order during MySQL pull Ensures database columns are sorted by their ordinal position during the introspection process. This maintains the original schema structure and provides a consistent output that matches the physical database layout. * refactor(cli): remove schema fields from MySQL queries Eliminates redundant schema and database name fields from the MySQL introspection query. Since MySQL does not support multi-schema architectures internal to a single connection in this context, removing these fields simplifies the data structure and avoids unnecessary metadata overhead. * fix(cli): improve MySQL introspection and index mapping Refines the database pull process to better handle MySQL-specific patterns. Improves unique constraint detection to prevent redundant mapping attributes when default naming conventions are used. Updates the MySQL introspection logic to correctly identify boolean types, handle timestamp precision in default values, and normalize numeric defaults. Also ensures auto-incrementing columns and primary key indexes are correctly mapped to prevent schema duplication. * test(cli): pass provider to default prelude in tests Ensures that the default schema prelude correctly reflects the database provider specified in test options. This prevents inconsistencies when generating test projects with non-default providers. * fix(cli): improve MySQL introspection for types and defaults Disables NativeEnum support for MySQL to prevent loss of schema-level enums since MySQL enums are column-specific. Refines boolean and numeric type mapping to better handle synthetic boolean types and preserve decimal precision in default values. Updates default value parsing logic to correctly identify unquoted strings and avoid misinterpreting numeric literals as booleans. * fix(cli): improve MySQL default value introspection Refines how default values are handled during database introspection for MySQL by considering the specific field type. This ensures that boolean variants and numeric literals for Float and Decimal types are correctly formatted and preserved. Also clarifies unsupported features in the SQLite provider to improve codebase maintainability. * test(cli): expand and reorganize db pull tests Enhances the test suite for the database pull command by adding comprehensive coverage for common schema features and PostgreSQL-specific functionality. Includes new test cases for: - Restoring complex schemas from scratch, including relations and indexes - Preserving existing imports in multi-file schema setups - Handling PostgreSQL-specific features like multi-schema support and native enums - Verifying schema preservation for field and table mappings The tests are restructured for better clarity across different database providers. * refactor: restructure introspection provider interface and attribute generation * feat: modernize MySQL introspection provider * feat: modernize PostgreSQL introspection provider * feat: modernize SQLite introspection provider * fix: improve relation field naming and default action handling * feat: track imports and auto-format during db pull * test: update pull tests to reflect naming and formatting improvements * fix(cli): refactor PostgreSQL type casting and fix index order Extracts PostgreSQL type casting logic into a reusable helper function to improve maintainability and ensure consistent attribute handling across all field types. Adjusts the table index sorting logic to better preserve the original database creation order while maintaining the priority of unique indexes. * fix(cli): filter out auto-generated MySQL indexes Prevents foreign key indexes created automatically by MySQL from appearing in the introspected schema. This ensures the output reflects manually defined indexes and avoids redundancy in schema definitions. * test(cli): support datasource extras in test utils Enhances the test utility helpers to allow passing extra datasource properties, such as multi-schema configurations for PostgreSQL. Refactors existing database pull tests to use these extra properties, ensuring the generated ZModel schema correctly reflects multi-schema environments while simplifying assertions. * fix: address PR comments * fix: address PR comments * fix: address PR comments * fix: address PR comments * fix(cli): improve file path resolution in pull action * refactor(cli): extract and enhance name casing logic * refactor(cli): consolidate default value normalization * feat(cli): improve enum syncing and relation naming during pull * docs(cli): add documentation comments to SQL introspection queries * test(cli): refactor test utilities and modernize test suites * fix(cli): improve db pull for composite FKs and MySQL uniqueness Enhances database introspection to correctly handle composite foreign keys by mapping columns by position rather than name alone. Improves MySQL introspection by checking statistics tables for single-column unique indexes, ensuring accurate model generation even when column keys are ambiguous. Ensures MySQL synthetic enum names respect requested model casing to prevent unnecessary schema mapping. Adds comprehensive tests for composite relations and database-specific uniqueness detection. * fix: address PR comments * fix(cli): improve SQLite introspection for untyped columns and composite FKs Ensures columns with no declared type are correctly mapped to Bytes following SQLite affinity rules, preventing them from being marked as Unsupported. Updates the DDL parser to correctly identify and map constraint names for composite foreign keys. This ensures that multi-column relations are properly restored during the pull process. Adds regression tests for both untyped columns and composite foreign key restoration. * feat(cli): pull generated/computed columns as Unsupported type Improves database introspection by identifying generated columns in MySQL, PostgreSQL, and SQLite. These columns are now pulled as `Unsupported` types containing their full DDL definition, preventing issues where read-only database fields were incorrectly treated as writable application-level fields. Includes normalization for expression formatting and a fix for string literal escaping in the code generator to ensure stable schema output. Relates to ZModel introspection consistency. * fix(cli): Use parameterized queries for MySQL introspection Switches from template literal interpolation to parameterized queries in MySQL introspection functions. This improves security by preventing potential SQL injection and ensures better handling of database names containing special characters. * fix(cli): use nullish coalescing for precision check --- packages/cli/package.json | 1 + packages/cli/src/actions/action-utils.ts | 25 +- packages/cli/src/actions/db.ts | 628 ++++++++- packages/cli/src/actions/pull/casing.ts | 43 + packages/cli/src/actions/pull/index.ts | 558 ++++++++ .../cli/src/actions/pull/provider/index.ts | 13 + .../cli/src/actions/pull/provider/mysql.ts | 597 +++++++++ .../src/actions/pull/provider/postgresql.ts | 653 ++++++++++ .../cli/src/actions/pull/provider/provider.ts | 96 ++ .../cli/src/actions/pull/provider/sqlite.ts | 477 +++++++ packages/cli/src/actions/pull/utils.ts | 214 ++++ packages/cli/src/index.ts | 30 + packages/cli/test/casing.test.ts | 130 ++ packages/cli/test/check.test.ts | 29 +- packages/cli/test/db.test.ts | 18 +- packages/cli/test/db/pull.test.ts | 1123 +++++++++++++++++ packages/cli/test/db/push.test.ts | 18 + packages/cli/test/format.test.ts | 8 +- packages/cli/test/generate.test.ts | 28 +- packages/cli/test/migrate.test.ts | 36 +- .../cli/test/plugins/custom-plugin.test.ts | 4 +- .../cli/test/plugins/prisma-plugin.test.ts | 20 +- packages/cli/test/utils.ts | 103 +- packages/language/package.json | 10 + packages/language/res/stdlib.zmodel | 6 +- packages/language/src/document.ts | 25 +- packages/language/src/factory/ast-factory.ts | 52 + packages/language/src/factory/attribute.ts | 281 +++++ packages/language/src/factory/declaration.ts | 373 ++++++ packages/language/src/factory/expression.ts | 308 +++++ packages/language/src/factory/index.ts | 5 + packages/language/src/factory/primitives.ts | 61 + .../src/validators/datamodel-validator.ts | 4 +- .../language/src/zmodel-code-generator.ts | 52 +- packages/language/tsup.config.ts | 1 + pnpm-lock.yaml | 9 +- 36 files changed, 5924 insertions(+), 115 deletions(-) create mode 100644 packages/cli/src/actions/pull/casing.ts create mode 100644 packages/cli/src/actions/pull/index.ts create mode 100644 packages/cli/src/actions/pull/provider/index.ts create mode 100644 packages/cli/src/actions/pull/provider/mysql.ts create mode 100644 packages/cli/src/actions/pull/provider/postgresql.ts create mode 100644 packages/cli/src/actions/pull/provider/provider.ts create mode 100644 packages/cli/src/actions/pull/provider/sqlite.ts create mode 100644 packages/cli/src/actions/pull/utils.ts create mode 100644 packages/cli/test/casing.test.ts create mode 100644 packages/cli/test/db/pull.test.ts create mode 100644 packages/cli/test/db/push.test.ts create mode 100644 packages/language/src/factory/ast-factory.ts create mode 100644 packages/language/src/factory/attribute.ts create mode 100644 packages/language/src/factory/declaration.ts create mode 100644 packages/language/src/factory/expression.ts create mode 100644 packages/language/src/factory/index.ts create mode 100644 packages/language/src/factory/primitives.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 750de5c68..ceb53e2fa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "@zenstackhq/common-helpers": "workspace:*", + "@zenstackhq/schema": "workspace:*", "@zenstackhq/language": "workspace:*", "@zenstackhq/orm": "workspace:*", "@zenstackhq/sdk": "workspace:*", diff --git a/packages/cli/src/actions/action-utils.ts b/packages/cli/src/actions/action-utils.ts index d2e0ca2e9..2e264593c 100644 --- a/packages/cli/src/actions/action-utils.ts +++ b/packages/cli/src/actions/action-utils.ts @@ -1,5 +1,5 @@ -import { loadDocument } from '@zenstackhq/language'; -import { isDataSource } from '@zenstackhq/language/ast'; +import { type ZModelServices, loadDocument } from '@zenstackhq/language'; +import { type Model, isDataSource } from '@zenstackhq/language/ast'; import { PrismaSchemaGenerator } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'node:fs'; @@ -41,8 +41,22 @@ export function getSchemaFile(file?: string) { } } -export async function loadSchemaDocument(schemaFile: string) { - const loadResult = await loadDocument(schemaFile); +export async function loadSchemaDocument( + schemaFile: string, + opts?: { mergeImports?: boolean; returnServices?: false }, +): Promise; +export async function loadSchemaDocument( + schemaFile: string, + opts: { returnServices: true; mergeImports?: boolean }, +): Promise<{ model: Model; services: ZModelServices }>; +export async function loadSchemaDocument( + schemaFile: string, + opts: { returnServices?: boolean; mergeImports?: boolean } = {}, +) { + const returnServices = opts.returnServices ?? false; + const mergeImports = opts.mergeImports ?? true; + + const loadResult = await loadDocument(schemaFile, [], mergeImports); if (!loadResult.success) { loadResult.errors.forEach((err) => { console.error(colors.red(err)); @@ -52,6 +66,9 @@ export async function loadSchemaDocument(schemaFile: string) { loadResult.warnings.forEach((warn) => { console.warn(colors.yellow(warn)); }); + + if (returnServices) return { model: loadResult.model, services: loadResult.services }; + return loadResult.model; } diff --git a/packages/cli/src/actions/db.ts b/packages/cli/src/actions/db.ts index 3d0108374..b868566a5 100644 --- a/packages/cli/src/actions/db.ts +++ b/packages/cli/src/actions/db.ts @@ -1,25 +1,54 @@ +import { formatDocument, ZModelCodeGenerator } from '@zenstackhq/language'; +import { DataModel, Enum, type Model } from '@zenstackhq/language/ast'; +import colors from 'colors'; import fs from 'node:fs'; +import path from 'node:path'; +import ora from 'ora'; import { execPrisma } from '../utils/exec-utils'; -import { generateTempPrismaSchema, getSchemaFile, handleSubProcessError, requireDataSourceUrl } from './action-utils'; +import { + generateTempPrismaSchema, + getSchemaFile, + handleSubProcessError, + loadSchemaDocument, + requireDataSourceUrl, +} from './action-utils'; +import { syncEnums, syncRelation, syncTable, type Relation } from './pull'; +import { providers as pullProviders } from './pull/provider'; +import { getDatasource, getDbName, getRelationFieldsKey, getRelationFkName } from './pull/utils'; +import type { DataSourceProviderType } from '@zenstackhq/schema'; +import { CliError } from '../cli-error'; -type Options = { +type PushOptions = { schema?: string; acceptDataLoss?: boolean; forceReset?: boolean; }; +export type PullOptions = { + schema?: string; + output?: string; + modelCasing: 'pascal' | 'camel' | 'snake' | 'none'; + fieldCasing: 'pascal' | 'camel' | 'snake' | 'none'; + alwaysMap: boolean; + quote: 'single' | 'double'; + indent: number; +}; + /** * CLI action for db related commands */ -export async function run(command: string, options: Options) { +export async function run(command: string, options: any) { switch (command) { case 'push': await runPush(options); break; + case 'pull': + await runPull(options); + break; } } -async function runPush(options: Options) { +async function runPush(options: PushOptions) { const schemaFile = getSchemaFile(options.schema); // validate datasource url exists @@ -49,3 +78,594 @@ async function runPush(options: Options) { } } } + +async function runPull(options: PullOptions) { + const spinner = ora(); + try { + const schemaFile = getSchemaFile(options.schema); + + // Determine early if `--out` is a single file output (combined schema) or a directory export. + const outPath = options.output ? path.resolve(options.output) : undefined; + const treatAsFile = + !!outPath && + ((fs.existsSync(outPath) && fs.lstatSync(outPath).isFile()) || path.extname(outPath) !== ''); + + const { model, services } = await loadSchemaDocument(schemaFile, { + returnServices: true, + mergeImports: treatAsFile, + }); + + const SUPPORTED_PROVIDERS = Object.keys(pullProviders) as DataSourceProviderType[]; + const datasource = getDatasource(model); + + if (!SUPPORTED_PROVIDERS.includes(datasource.provider)) { + throw new CliError(`Unsupported datasource provider: ${datasource.provider}`); + } + + const provider = pullProviders[datasource.provider]; + + if (!provider) { + throw new CliError(`No introspection provider found for: ${datasource.provider}`); + } + + spinner.start('Introspecting database...'); + const { enums, tables } = await provider.introspect(datasource.url, { schemas: datasource.allSchemas, modelCasing: options.modelCasing }); + spinner.succeed('Database introspected'); + + console.log(colors.blue('Syncing schema...')); + + const newModel: Model = { + $type: 'Model', + $container: undefined, + $containerProperty: undefined, + $containerIndex: undefined, + declarations: [...model.declarations.filter((d) => ['DataSource'].includes(d.$type))], + imports: model.imports, + }; + + syncEnums({ + dbEnums: enums, + model: newModel, + services, + options, + defaultSchema: datasource.defaultSchema, + oldModel: model, + provider, + }); + + const resolvedRelations: Relation[] = []; + for (const table of tables) { + const relations = syncTable({ + table, + model: newModel, + provider, + services, + options, + defaultSchema: datasource.defaultSchema, + oldModel: model, + }); + resolvedRelations.push(...relations); + } + // sync relation fields + for (const relation of resolvedRelations) { + const similarRelations = resolvedRelations.filter((rr) => { + return ( + rr !== relation && + ((rr.schema === relation.schema && + rr.table === relation.table && + rr.references.schema === relation.references.schema && + rr.references.table === relation.references.table) || + (rr.schema === relation.references.schema && + rr.columns[0] === relation.references.columns[0] && + rr.references.schema === relation.schema && + rr.references.table === relation.table)) + ); + }).length; + const selfRelation = + relation.references.schema === relation.schema && relation.references.table === relation.table; + syncRelation({ + model: newModel, + relation, + services, + options, + selfRelation, + similarRelations: similarRelations, + }); + } + + console.log(colors.blue('Schema synced')); + + const baseDir = path.dirname(path.resolve(schemaFile)); + const baseDirUrlPath = new URL(`file://${baseDir}`).pathname; + const docs = services.shared.workspace.LangiumDocuments.all + .filter(({ uri }) => uri.path.toLowerCase().startsWith(baseDirUrlPath.toLowerCase())) + .toArray(); + const docsSet = new Set(docs.map((d) => d.uri.toString())); + + console.log(colors.bold('\nApplying changes to ZModel...')); + + const deletedModels: string[] = []; + const deletedEnums: string[] = []; + const addedModels: string[] = []; + const addedEnums: string[] = []; + // Hierarchical change tracking: model -> field changes -> attribute changes + type ModelChanges = { + addedFields: string[]; + deletedFields: string[]; + updatedFields: string[]; + addedAttributes: string[]; + deletedAttributes: string[]; + updatedAttributes: string[]; + }; + const modelChanges = new Map(); + + const getModelChanges = (modelName: string): ModelChanges => { + if (!modelChanges.has(modelName)) { + modelChanges.set(modelName, { + addedFields: [], + deletedFields: [], + updatedFields: [], + addedAttributes: [], + deletedAttributes: [], + updatedAttributes: [], + }); + } + return modelChanges.get(modelName)!; + }; + + // Delete models + services.shared.workspace.IndexManager.allElements('DataModel', docsSet) + .filter( + (declaration) => + !newModel.declarations.find((d) => getDbName(d) === getDbName(declaration.node as any)), + ) + .forEach((decl) => { + const model = decl.node!.$container as Model; + const index = model.declarations.findIndex((d) => d === decl.node); + model.declarations.splice(index, 1); + deletedModels.push(colors.red(`- Model ${decl.name} deleted`)); + }); + + // Delete Enums + if (provider.isSupportedFeature('NativeEnum')) + services.shared.workspace.IndexManager.allElements('Enum', docsSet) + .filter( + (declaration) => + !newModel.declarations.find((d) => getDbName(d) === getDbName(declaration.node as any)), + ) + .forEach((decl) => { + const model = decl.node!.$container as Model; + const index = model.declarations.findIndex((d) => d === decl.node); + model.declarations.splice(index, 1); + deletedEnums.push(colors.red(`- Enum ${decl.name} deleted`)); + }); + // Add/update models and their fields + newModel.declarations + .filter((d) => [DataModel, Enum].includes(d.$type)) + .forEach((_declaration) => { + const newDataModel = _declaration as DataModel | Enum; + const declarations = services.shared.workspace.IndexManager.allElements(newDataModel.$type, docsSet).toArray(); + const originalDataModel = declarations.find((d) => getDbName(d.node as any) === getDbName(newDataModel)) + ?.node as DataModel | Enum | undefined; + if (!originalDataModel) { + + if (newDataModel.$type === 'DataModel') { + addedModels.push(colors.green(`+ Model ${newDataModel.name} added`)); + } else if (newDataModel.$type === 'Enum') { + addedEnums.push(colors.green(`+ Enum ${newDataModel.name} added`)); + } + + model.declarations.push(newDataModel); + (newDataModel as any).$container = model; + newDataModel.fields.forEach((f) => { + if (f.$type === 'DataField' && f.type.reference?.ref) { + const ref = declarations.find( + (d) => getDbName(d.node as any) === getDbName(f.type.reference!.ref as any), + )?.node; + if (ref && f.type.reference) { + // Replace the entire reference object — Langium References + // from parsed documents expose `ref` as a getter-only property. + (f.type as any).reference = { + ref, + $refText: (ref as any).name ?? (f.type.reference as any).$refText, + }; + } + } + }); + return; + } + + newDataModel.fields.forEach((f) => { + // Prioritized matching: exact db name > relation fields key > relation FK name > type reference + let originalFields = originalDataModel.fields.filter((d) => getDbName(d) === getDbName(f)); + + // If this is a back-reference relation field (has @relation but no `fields` arg), silently skip + const isRelationField = + f.$type === 'DataField' && !!(f as any).attributes?.some((a: any) => a?.decl?.ref?.name === '@relation'); + if (originalFields.length === 0 && isRelationField && !getRelationFieldsKey(f as any)) { + return; + } + + if (originalFields.length === 0) { + // Try matching by relation fields key (the `fields` attribute in @relation) + // This matches relation fields by their FK field references + const newFieldsKey = getRelationFieldsKey(f as any); + if (newFieldsKey) { + originalFields = originalDataModel.fields.filter( + (d) => getRelationFieldsKey(d as any) === newFieldsKey, + ); + } + } + + if (originalFields.length === 0) { + // Try matching by relation FK name (the `map` attribute in @relation) + originalFields = originalDataModel.fields.filter( + (d) => + getRelationFkName(d as any) === getRelationFkName(f as any) && + !!getRelationFkName(d as any) && + !!getRelationFkName(f as any), + ); + } + + if (originalFields.length === 0) { + // Try matching by type reference + // We need this because for relations that don't have @relation, we can only check if the original exists by the field type. + // Yes, in this case it can potentially result in multiple original fields, but we only want to ensure that at least one relation exists. + // In the future, we might implement some logic to detect how many of these types of relations we need and add/remove fields based on this. + originalFields = originalDataModel.fields.filter( + (d) => + f.$type === 'DataField' && + d.$type === 'DataField' && + f.type.reference?.ref && + d.type.reference?.ref && + getDbName(f.type.reference.ref) === getDbName(d.type.reference.ref), + ); + } + + if (originalFields.length > 1) { + // If this is a back-reference relation field (no `fields` attribute), + // silently skip when there are multiple potential matches + const isBackReferenceField = !getRelationFieldsKey(f as any); + if (!isBackReferenceField) { + console.warn( + colors.yellow( + `Found more original fields, need to tweak the search algorithm. ${originalDataModel.name}->[${originalFields.map((of) => of.name).join(', ')}](${f.name})`, + ), + ); + } + return; + } + const originalField = originalFields.at(0); + + // Update existing field if type, optionality, or array flag changed + if (originalField && f.$type === 'DataField' && originalField.$type === 'DataField') { + const newType = f.type; + const oldType = originalField.type; + const fieldUpdates: string[] = []; + + // Check and update builtin type (e.g., String -> Int) + // Skip if old type is an Enum reference and provider doesn't support native enums + const isOldTypeEnumWithoutNativeSupport = + oldType.reference?.ref?.$type === 'Enum' && !provider.isSupportedFeature('NativeEnum'); + if (newType.type && oldType.type !== newType.type && !isOldTypeEnumWithoutNativeSupport) { + fieldUpdates.push(`type: ${oldType.type} -> ${newType.type}`); + (oldType as any).type = newType.type; + } + + // Check and update type reference (e.g., User -> Profile) + if (newType.reference?.ref && oldType.reference?.ref) { + const newRefName = getDbName(newType.reference.ref); + const oldRefName = getDbName(oldType.reference.ref); + if (newRefName !== oldRefName) { + fieldUpdates.push(`reference: ${oldType.reference.$refText} -> ${newType.reference.$refText}`); + // Replace the entire reference object — Langium References + // from parsed documents expose `ref` as a getter-only property. + (oldType as any).reference = { + ref: newType.reference.ref, + $refText: newType.reference.$refText, + }; + } + } else if (newType.reference?.ref && !oldType.reference) { + // Changed from builtin to reference type + fieldUpdates.push(`type: ${oldType.type} -> ${newType.reference.$refText}`); + (oldType as any).reference = newType.reference; + (oldType as any).type = undefined; + } else if (!newType.reference && oldType.reference?.ref && newType.type) { + // Changed from reference to builtin type + // Skip if old type is an Enum and provider doesn't support native enums (e.g., SQLite stores enums as strings) + const isEnumWithoutNativeSupport = + oldType.reference.ref.$type === 'Enum' && !provider.isSupportedFeature('NativeEnum'); + if (!isEnumWithoutNativeSupport) { + fieldUpdates.push(`type: ${oldType.reference.$refText} -> ${newType.type}`); + (oldType as any).type = newType.type; + (oldType as any).reference = undefined; + } + } + + // Check and update optionality (e.g., String -> String?) + if (!!newType.optional !== !!oldType.optional) { + fieldUpdates.push(`optional: ${!!oldType.optional} -> ${!!newType.optional}`); + (oldType as any).optional = newType.optional; + } + + // Check and update array flag (e.g., String -> String[]) + if (!!newType.array !== !!oldType.array) { + fieldUpdates.push(`array: ${!!oldType.array} -> ${!!newType.array}`); + (oldType as any).array = newType.array; + } + + if (fieldUpdates.length > 0) { + getModelChanges(originalDataModel.name).updatedFields.push( + colors.yellow(`~ ${originalField.name} (${fieldUpdates.join(', ')})`), + ); + } + + // Update @default attribute arguments if changed + const newDefaultAttr = f.attributes.find((a) => a.decl.$refText === '@default'); + const oldDefaultAttr = originalField.attributes.find((a) => a.decl.$refText === '@default'); + if (newDefaultAttr && oldDefaultAttr) { + // Compare attribute arguments by serializing them (avoid circular refs with $type fallback) + const serializeArgs = (args: any[]) => + args.map((arg) => { + if (arg.value?.$type === 'StringLiteral') return `"${arg.value.value}"`; + if (arg.value?.$type === 'NumberLiteral') return String(arg.value.value); + if (arg.value?.$type === 'BooleanLiteral') return String(arg.value.value); + if (arg.value?.$type === 'InvocationExpr') return arg.value.function?.$refText ?? ''; + if (arg.value?.$type === 'ReferenceExpr') return arg.value.target?.$refText ?? ''; + if (arg.value?.$type === 'ArrayExpr') { + return `[${(arg.value.items ?? []).map((item: any) => { + if (item.$type === 'ReferenceExpr') return item.target?.$refText ?? ''; + return item.$type ?? 'unknown'; + }).join(',')}]`; + } + // Fallback: use $type to avoid circular reference issues + return arg.value?.$type ?? 'unknown'; + }).join(','); + + const newArgsStr = serializeArgs(newDefaultAttr.args ?? []); + const oldArgsStr = serializeArgs(oldDefaultAttr.args ?? []); + + if (newArgsStr !== oldArgsStr) { + // Replace old @default arguments with new ones + (oldDefaultAttr as any).args = newDefaultAttr.args.map((arg) => ({ + ...arg, + $container: oldDefaultAttr, + })); + getModelChanges(originalDataModel.name).updatedAttributes.push( + colors.yellow(`~ @default on ${originalDataModel.name}.${originalField.name}`), + ); + } + } + } + + if (!originalField) { + getModelChanges(originalDataModel.name).addedFields.push(colors.green(`+ ${f.name}`)); + (f as any).$container = originalDataModel; + originalDataModel.fields.push(f as any); + if (f.$type === 'DataField' && f.type.reference?.ref) { + const ref = declarations.find( + (d) => getDbName(d.node as any) === getDbName(f.type.reference!.ref as any), + )?.node as DataModel | undefined; + if (ref) { + // Replace the entire reference object — Langium References + // from parsed documents expose `ref` as a getter-only property. + (f.type as any).reference = { + ref, + $refText: ref.name ?? (f.type.reference as any).$refText, + }; + } + } + return; + } + // Track deleted attributes (in original but not in new) + originalField.attributes + .filter( + (attr) => + !f.attributes.find((d) => d.decl.$refText === attr.decl.$refText) && + !['@map', '@@map', '@default', '@updatedAt'].includes(attr.decl.$refText), + ) + .forEach((attr) => { + const field = attr.$container; + const index = field.attributes.findIndex((d) => d === attr); + field.attributes.splice(index, 1); + getModelChanges(originalDataModel.name).deletedAttributes.push( + colors.yellow(`- ${attr.decl.$refText} from field: ${originalDataModel.name}.${field.name}`), + ); + }); + + // Track added attributes (in new but not in original) + f.attributes + .filter( + (attr) => + !originalField.attributes.find((d) => d.decl.$refText === attr.decl.$refText) && + !['@map', '@@map', '@default', '@updatedAt'].includes(attr.decl.$refText), + ) + .forEach((attr) => { + // attach the new attribute to the original field + const cloned = { ...attr, $container: originalField } as typeof attr; + originalField.attributes.push(cloned); + getModelChanges(originalDataModel.name).addedAttributes.push( + colors.green(`+ ${attr.decl.$refText} to field: ${originalDataModel.name}.${f.name}`), + ); + }); + }); + originalDataModel.fields + .filter((f) => { + // Prioritized matching: exact db name > relation fields key > relation FK name > type reference + const matchByDbName = newDataModel.fields.find((d) => getDbName(d) === getDbName(f)); + if (matchByDbName) return false; + + // Try matching by relation fields key (the `fields` attribute in @relation) + const originalFieldsKey = getRelationFieldsKey(f as any); + if (originalFieldsKey) { + const matchByFieldsKey = newDataModel.fields.find( + (d) => getRelationFieldsKey(d as any) === originalFieldsKey, + ); + if (matchByFieldsKey) return false; + } + + const matchByFkName = newDataModel.fields.find( + (d) => + getRelationFkName(d as any) === getRelationFkName(f as any) && + !!getRelationFkName(d as any) && + !!getRelationFkName(f as any), + ); + if (matchByFkName) return false; + + const matchByTypeRef = newDataModel.fields.find( + (d) => + f.$type === 'DataField' && + d.$type === 'DataField' && + f.type.reference?.ref && + d.type.reference?.ref && + getDbName(f.type.reference.ref) === getDbName(d.type.reference.ref), + ); + return !matchByTypeRef; + }) + .forEach((f) => { + const _model = f.$container; + const index = _model.fields.findIndex((d) => d === f); + _model.fields.splice(index, 1); + getModelChanges(_model.name).deletedFields.push(colors.red(`- ${f.name}`)); + }); + }); + + if (deletedModels.length > 0) { + console.log(colors.bold('\nDeleted Models:')); + deletedModels.forEach((msg) => { + console.log(msg); + }); + } + + if (deletedEnums.length > 0) { + console.log(colors.bold('\nDeleted Enums:')); + deletedEnums.forEach((msg) => { + console.log(msg); + }); + } + + if (addedModels.length > 0) { + console.log(colors.bold('\nAdded Models:')); + addedModels.forEach((msg) => { + console.log(msg); + }); + } + + if (addedEnums.length > 0) { + console.log(colors.bold('\nAdded Enums:')); + addedEnums.forEach((msg) => { + console.log(msg); + }); + } + + // Print hierarchical model changes + if (modelChanges.size > 0) { + console.log(colors.bold('\nModel Changes:')); + modelChanges.forEach((changes, modelName) => { + const hasChanges = + changes.addedFields.length > 0 || + changes.deletedFields.length > 0 || + changes.updatedFields.length > 0 || + changes.addedAttributes.length > 0 || + changes.deletedAttributes.length > 0 || + changes.updatedAttributes.length > 0; + + if (hasChanges) { + console.log(colors.cyan(` ${modelName}:`)); + + if (changes.addedFields.length > 0) { + console.log(colors.gray(' Added Fields:')); + changes.addedFields.forEach((msg) => { + console.log(` ${msg}`); + }); + } + + if (changes.deletedFields.length > 0) { + console.log(colors.gray(' Deleted Fields:')); + changes.deletedFields.forEach((msg) => { + console.log(` ${msg}`); + }); + } + + if (changes.updatedFields.length > 0) { + console.log(colors.gray(' Updated Fields:')); + changes.updatedFields.forEach((msg) => { + console.log(` ${msg}`); + }); + } + + if (changes.addedAttributes.length > 0) { + console.log(colors.gray(' Added Attributes:')); + changes.addedAttributes.forEach((msg) => { + console.log(` ${msg}`); + }); + } + + if (changes.deletedAttributes.length > 0) { + console.log(colors.gray(' Deleted Attributes:')); + changes.deletedAttributes.forEach((msg) => { + console.log(` ${msg}`); + }); + } + + if (changes.updatedAttributes.length > 0) { + console.log(colors.gray(' Updated Attributes:')); + changes.updatedAttributes.forEach((msg) => { + console.log(` ${msg}`); + }); + } + } + }); + } + + const generator = new ZModelCodeGenerator({ + quote: options.quote, + indent: options.indent, + }); + + if (options.output) { + if (treatAsFile) { + const zmodelSchema = await formatDocument(generator.generate(newModel)); + console.log(colors.blue(`Writing to ${outPath}`)); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, zmodelSchema); + } else { + // Otherwise treat `--out` as a directory path. Create it if needed. + fs.mkdirSync(outPath!, { recursive: true }); + + // Preserve the directory structure relative to the schema file location (options.schema base). + const baseDir = path.dirname(path.resolve(schemaFile)); + + for (const { + uri, + parseResult: { value: documentModel }, + } of docs) { + const zmodelSchema = await formatDocument(generator.generate(documentModel)); + + // Map input file path -> output file path under `--out` + const relPath = path.relative(baseDir, uri.fsPath); + const targetFile = path.join(outPath!, relPath); + + fs.mkdirSync(path.dirname(targetFile), { recursive: true }); + console.log(colors.blue(`Writing to ${targetFile}`)); + fs.writeFileSync(targetFile, zmodelSchema); + } + } + } else { + for (const { + uri, + parseResult: { value: documentModel }, + } of docs) { + const zmodelSchema = await formatDocument(generator.generate(documentModel)); + console.log(colors.blue(`Writing to ${path.relative(process.cwd(), uri.fsPath).replace(/\\/g, '/')}`)); + fs.writeFileSync(uri.fsPath, zmodelSchema); + } + } + + console.log(colors.green.bold('\nPull completed successfully!')); + } catch (error) { + spinner.fail('Pull failed'); + console.error(error); + throw error; + } +} diff --git a/packages/cli/src/actions/pull/casing.ts b/packages/cli/src/actions/pull/casing.ts new file mode 100644 index 000000000..5e0846bc9 --- /dev/null +++ b/packages/cli/src/actions/pull/casing.ts @@ -0,0 +1,43 @@ +export function resolveNameCasing(casing: 'pascal' | 'camel' | 'snake' | 'none', originalName: string) { + let name = originalName; + const fieldPrefix = /[0-9]/g.test(name.charAt(0)) ? '_' : ''; + + switch (casing) { + case 'pascal': + name = toPascalCase(originalName); + break; + case 'camel': + name = toCamelCase(originalName); + break; + case 'snake': + name = toSnakeCase(originalName); + break; + } + + return { + modified: name !== originalName || fieldPrefix !== '', + name: `${fieldPrefix}${name}`, + }; +} + +function isAllUpperCase(str: string): boolean { + return str === str.toUpperCase(); +} + +export function toPascalCase(str: string): string { + if (isAllUpperCase(str)) return str; + return str.replace(/[_\- ]+(\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase()); +} + +export function toCamelCase(str: string): string { + if (isAllUpperCase(str)) return str; + return str.replace(/[_\- ]+(\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toLowerCase()); +} + +export function toSnakeCase(str: string): string { + if (isAllUpperCase(str)) return str; + return str + .replace(/[- ]+/g, '_') + .replace(/([a-z0-9])([A-Z])/g, '$1_$2') + .toLowerCase(); +} diff --git a/packages/cli/src/actions/pull/index.ts b/packages/cli/src/actions/pull/index.ts new file mode 100644 index 000000000..b4424746f --- /dev/null +++ b/packages/cli/src/actions/pull/index.ts @@ -0,0 +1,558 @@ +import type { ZModelServices } from '@zenstackhq/language'; +import colors from 'colors'; +import { + isEnum, + type DataField, + type DataModel, + type Enum, + type Model, +} from '@zenstackhq/language/ast'; +import { + DataFieldAttributeFactory, + DataFieldFactory, + DataModelFactory, + EnumFactory, +} from '@zenstackhq/language/factory'; +import { AstUtils, type Reference, type AstNode, type CstNode } from 'langium'; +import { lowerCaseFirst } from '@zenstackhq/common-helpers'; +import type { PullOptions } from '../db'; +import type { Cascade, IntrospectedEnum, IntrospectedTable, IntrospectionProvider } from './provider'; +import { getAttributeRef, getDbName, getEnumRef } from './utils'; +import { resolveNameCasing } from './casing'; +import { CliError } from '../../cli-error'; + +export function syncEnums({ + dbEnums, + model, + oldModel, + provider, + options, + services, + defaultSchema, +}: { + dbEnums: IntrospectedEnum[]; + model: Model; + oldModel: Model; + provider: IntrospectionProvider; + services: ZModelServices; + options: PullOptions; + defaultSchema: string; +}) { + if (provider.isSupportedFeature('NativeEnum')) { + for (const dbEnum of dbEnums) { + const { modified, name } = resolveNameCasing(options.modelCasing, dbEnum.enum_type); + if (modified) console.log(colors.gray(`Mapping enum ${dbEnum.enum_type} to ${name}`)); + const factory = new EnumFactory().setName(name); + if (modified || options.alwaysMap) + factory.addAttribute((builder) => + builder + .setDecl(getAttributeRef('@@map', services)) + .addArg((argBuilder) => argBuilder.StringLiteral.setValue(dbEnum.enum_type)), + ); + + dbEnum.values.forEach((v) => { + const { name, modified } = resolveNameCasing(options.fieldCasing, v); + factory.addField((builder) => { + builder.setName(name); + if (modified || options.alwaysMap) + builder.addAttribute((builder) => + builder + .setDecl(getAttributeRef('@map', services)) + .addArg((argBuilder) => argBuilder.StringLiteral.setValue(v)), + ); + + return builder; + }); + }); + + if (dbEnum.schema_name && dbEnum.schema_name !== '' && dbEnum.schema_name !== defaultSchema) { + factory.addAttribute((b) => + b + .setDecl(getAttributeRef('@@schema', services)) + .addArg((a) => a.StringLiteral.setValue(dbEnum.schema_name)), + ); + } + + model.declarations.push(factory.get({ $container: model })); + } + } else { + // For providers that don't support native enums (e.g., SQLite), carry over + // enum declarations from the existing schema as-is by deep-cloning the AST nodes. + // A dummy buildReference is used since we don't need cross-reference resolution. + const dummyBuildReference = (_node: AstNode, _property: string, _refNode: CstNode | undefined, refText: string): Reference => + ({ $refText: refText }) as Reference; + + oldModel.declarations + .filter((d) => isEnum(d)) + .forEach((d) => { + const copy = AstUtils.copyAstNode(d, dummyBuildReference); + (copy as { $container: unknown }).$container = model; + model.declarations.push(copy); + }); + } +} + +export type Relation = { + schema: string; + table: string; + columns: string[]; + type: 'one' | 'many'; + fk_name: string; + foreign_key_on_update: Cascade; + foreign_key_on_delete: Cascade; + nullable: boolean; + references: { + schema: string | null; + table: string | null; + columns: (string | null)[]; + type: 'one' | 'many'; + }; +}; + +export function syncTable({ + model, + provider, + table, + services, + options, + defaultSchema, +}: { + table: IntrospectedTable; + model: Model; + oldModel: Model; + provider: IntrospectionProvider; + services: ZModelServices; + options: PullOptions; + defaultSchema: string; +}) { + const idAttribute = getAttributeRef('@id', services); + const modelIdAttribute = getAttributeRef('@@id', services); + const uniqueAttribute = getAttributeRef('@unique', services); + const modelUniqueAttribute = getAttributeRef('@@unique', services); + const fieldMapAttribute = getAttributeRef('@map', services); + const tableMapAttribute = getAttributeRef('@@map', services); + const modelindexAttribute = getAttributeRef('@@index', services); + + const relations: Relation[] = []; + const { name, modified } = resolveNameCasing(options.modelCasing, table.name); + const multiPk = table.columns.filter((c) => c.pk).length > 1; + + const modelFactory = new DataModelFactory().setName(name).setIsView(table.type === 'view'); + modelFactory.setContainer(model); + + if (modified || options.alwaysMap) { + modelFactory.addAttribute((builder) => + builder.setDecl(tableMapAttribute).addArg((argBuilder) => argBuilder.StringLiteral.setValue(table.name)), + ); + } + // Group FK columns by constraint name to handle composite foreign keys. + // Each FK constraint (identified by fk_name) may span multiple columns. + const fkGroups = new Map(); + table.columns.forEach((column) => { + if (column.foreign_key_table && column.foreign_key_name) { + const group = fkGroups.get(column.foreign_key_name) ?? []; + group.push(column); + fkGroups.set(column.foreign_key_name, group); + } + }); + + for (const [fkName, fkColumns] of fkGroups) { + const firstCol = fkColumns[0]!; + // For single-column FKs, check if the column is the table's single-column PK (one-to-one) + const isSingleColumnPk = fkColumns.length === 1 && !multiPk && firstCol.pk; + // A single-column FK with unique constraint means one-to-one on the opposite side + const isUniqueRelation = (fkColumns.length === 1 && firstCol.unique) || isSingleColumnPk; + relations.push({ + schema: table.schema, + table: table.name, + columns: fkColumns.map((c) => c.name), + type: 'one', + fk_name: fkName, + foreign_key_on_delete: firstCol.foreign_key_on_delete, + foreign_key_on_update: firstCol.foreign_key_on_update, + nullable: firstCol.nullable, + references: { + schema: firstCol.foreign_key_schema, + table: firstCol.foreign_key_table, + columns: fkColumns.map((c) => c.foreign_key_column), + type: isUniqueRelation ? 'one' : 'many', + }, + }); + } + + table.columns.forEach((column) => { + + const { name, modified } = resolveNameCasing(options.fieldCasing, column.name); + + const builtinType = provider.getBuiltinType(column.datatype); + + modelFactory.addField((builder) => { + builder.setName(name); + builder.setType((typeBuilder) => { + typeBuilder.setArray(builtinType.isArray); + // Array fields cannot be optional (Prisma/ZenStack limitation) + typeBuilder.setOptional(builtinType.isArray ? false : column.nullable); + + if (column.computed) { + // Generated/computed columns (e.g., GENERATED ALWAYS AS ... STORED/VIRTUAL) + // are read-only and must be rendered as Unsupported("full type definition"). + // The datatype contains the full DDL type definition including the expression. + typeBuilder.setUnsupported((unsupportedBuilder) => + unsupportedBuilder.setValue((lt) => lt.StringLiteral.setValue(column.datatype)), + ); + } else if (column.datatype === 'enum') { + const ref = model.declarations.find((d) => isEnum(d) && getDbName(d) === column.datatype_name) as + | Enum + | undefined; + + if (!ref) { + throw new CliError(`Enum ${column.datatype_name} not found`); + } + typeBuilder.setReference(ref); + } else { + if (builtinType.type !== 'Unsupported') { + typeBuilder.setType(builtinType.type); + } else { + typeBuilder.setUnsupported((unsupportedBuilder) => + unsupportedBuilder.setValue((lt) => lt.StringLiteral.setValue(column.datatype)), + ); + } + } + + return typeBuilder; + }); + + if (column.pk && !multiPk) { + builder.addAttribute((b) => b.setDecl(idAttribute)); + } + + // Add field-type-based attributes (e.g., @updatedAt for DateTime fields, @db.* attributes) + const fieldAttrs = provider.getFieldAttributes({ + fieldName: column.name, + fieldType: builtinType.type, + datatype: column.datatype, + length: column.length, + precision: column.precision, + services, + }); + fieldAttrs.forEach(builder.addAttribute.bind(builder)); + + if (column.default && !column.computed) { + const defaultExprBuilder = provider.getDefaultValue({ + fieldType: builtinType.type, + datatype: column.datatype, + datatype_name: column.datatype_name, + defaultValue: column.default, + services, + enums: model.declarations.filter((d) => d.$type === 'Enum') as Enum[], + }); + if (defaultExprBuilder) { + const defaultAttr = new DataFieldAttributeFactory() + .setDecl(getAttributeRef('@default', services)) + .addArg(defaultExprBuilder); + builder.addAttribute(defaultAttr); + } + } + + if (column.unique && !column.pk) { + builder.addAttribute((b) => { + b.setDecl(uniqueAttribute); + // Only add map if the unique constraint name differs from default patterns + // Default patterns: TableName_columnName_key (Prisma) or just columnName (MySQL) + const isDefaultName = !column.unique_name + || column.unique_name === `${table.name}_${column.name}_key` + || column.unique_name === column.name; + if (!isDefaultName) { + b.addArg((ab) => ab.StringLiteral.setValue(column.unique_name!), 'map'); + } + + return b; + }); + } + if (modified || options.alwaysMap) { + builder.addAttribute((ab) => + ab.setDecl(fieldMapAttribute).addArg((ab) => ab.StringLiteral.setValue(column.name)), + ); + } + + return builder; + }); + }); + + const pkColumns = table.columns.filter((c) => c.pk).map((c) => c.name); + if (multiPk) { + modelFactory.addAttribute((builder) => + builder.setDecl(modelIdAttribute).addArg((argBuilder) => { + const arrayExpr = argBuilder.ArrayExpr; + pkColumns.forEach((c) => { + const ref = modelFactory.node.fields.find((f) => getDbName(f) === c); + if (!ref) { + throw new CliError(`Field ${c} not found`); + } + arrayExpr.addItem((itemBuilder) => itemBuilder.ReferenceExpr.setTarget(ref)); + }); + return arrayExpr; + }), + ); + } + + const hasUniqueConstraint = + table.columns.some((c) => c.unique || c.pk) || + table.indexes.some((i) => i.unique); + if (!hasUniqueConstraint) { + modelFactory.addAttribute((a) => a.setDecl(getAttributeRef('@@ignore', services))); + modelFactory.addComment( + '/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Zenstack Client.', + ); + } + + // Sort indexes: unique indexes first, then other indexes + const sortedIndexes = table.indexes.reverse().sort((a, b) => { + if (a.unique && !b.unique) return -1; + if (!a.unique && b.unique) return 1; + return 0; + }); + + sortedIndexes.forEach((index) => { + if (index.predicate) { + //These constraints are not supported by Zenstack, because Zenstack currently does not fully support check constraints. Read more: https://pris.ly/d/check-constraints + console.warn( + colors.yellow( + `These constraints are not supported by Zenstack. Read more: https://pris.ly/d/check-constraints\n- Model: "${table.name}", constraint: "${index.name}"`, + ), + ); + return; + } + if (index.columns.find((c) => c.expression)) { + console.warn( + colors.yellow( + `These constraints are not supported by Zenstack. Read more: https://pris.ly/d/check-constraints\n- Model: "${table.name}", constraint: "${index.name}"`, + ), + ); + return; + } + + // Skip PRIMARY key index (handled via @id or @@id) + if (index.primary) { + return; + } + + // Skip single-column indexes that are already handled by @id or @unique on the field + if (index.columns.length === 1 && (index.columns.find((c) => pkColumns.includes(c.name)) || index.unique)) { + return; + } + + modelFactory.addAttribute((builder) => + { + const attr = builder + .setDecl(index.unique ? modelUniqueAttribute : modelindexAttribute) + .addArg((argBuilder) => { + const arrayExpr = argBuilder.ArrayExpr; + index.columns.forEach((c) => { + const ref = modelFactory.node.fields.find((f) => getDbName(f) === c.name); + if (!ref) { + throw new CliError(`Column ${c.name} not found in model ${table.name}`); + } + arrayExpr.addItem((itemBuilder) => { + const refExpr = itemBuilder.ReferenceExpr.setTarget(ref); + if (c.order && c.order !== 'ASC') + refExpr.addArg((ab) => ab.StringLiteral.setValue('DESC'), 'sort'); + + return refExpr; + }); + }); + return arrayExpr; + }); + + const suffix = index.unique ? '_key' : '_idx'; + + if(index.name !== `${table.name}_${index.columns.map(c => c.name).join('_')}${suffix}`){ + attr.addArg((argBuilder) => argBuilder.StringLiteral.setValue(index.name), 'map'); + } + + return attr + } + + ); + }); + if (table.schema && table.schema !== '' && table.schema !== defaultSchema) { + modelFactory.addAttribute((b) => + b.setDecl(getAttributeRef('@@schema', services)).addArg((a) => a.StringLiteral.setValue(table.schema)), + ); + } + + model.declarations.push(modelFactory.node); + return relations; +} + +export function syncRelation({ + model, + relation, + services, + options, + selfRelation, + similarRelations, +}: { + model: Model; + relation: Relation; + services: ZModelServices; + options: PullOptions; + //self included + similarRelations: number; + selfRelation: boolean; +}) { + const idAttribute = getAttributeRef('@id', services); + const uniqueAttribute = getAttributeRef('@unique', services); + const relationAttribute = getAttributeRef('@relation', services); + const fieldMapAttribute = getAttributeRef('@map', services); + const tableMapAttribute = getAttributeRef('@@map', services); + + const includeRelationName = selfRelation || similarRelations > 0; + + if (!idAttribute || !uniqueAttribute || !relationAttribute || !fieldMapAttribute || !tableMapAttribute) { + throw new CliError('Cannot find required attributes in the model.'); + } + + const sourceModel = model.declarations.find((d) => d.$type === 'DataModel' && getDbName(d) === relation.table) as + | DataModel + | undefined; + if (!sourceModel) return; + + // Resolve all source and target fields for the relation (supports composite FKs) + const sourceFields: { field: DataField; index: number }[] = []; + for (const colName of relation.columns) { + const idx = sourceModel.fields.findIndex((f) => getDbName(f) === colName); + const field = sourceModel.fields[idx] as DataField | undefined; + if (!field) return; + sourceFields.push({ field, index: idx }); + } + + const targetModel = model.declarations.find( + (d) => d.$type === 'DataModel' && getDbName(d) === relation.references.table, + ) as DataModel | undefined; + if (!targetModel) return; + + const targetFields: DataField[] = []; + for (const colName of relation.references.columns) { + const field = targetModel.fields.find((f) => getDbName(f) === colName); + if (!field) return; + targetFields.push(field); + } + + // Use the first source field for naming heuristics + const firstSourceField = sourceFields[0]!.field; + const firstSourceFieldId = sourceFields[0]!.index; + const firstColumn = relation.columns[0]!; + + const fieldPrefix = /[0-9]/g.test(sourceModel.name.charAt(0)) ? '_' : ''; + + const relationName = `${relation.table}${similarRelations > 0 ? `_${firstColumn}` : ''}To${relation.references.table}`; + + // Derive a relation field name from the FK scalar field: if the field ends with "Id", + // strip the suffix and use the remainder (e.g., "authorId" -> "author"). + const sourceNameFromReference = firstSourceField.name.toLowerCase().endsWith('id') ? `${resolveNameCasing(options.fieldCasing, firstSourceField.name.slice(0, -2)).name}${relation.type === 'many'? 's' : ''}` : undefined; + + // Check if the derived name would clash with an existing field + const sourceFieldFromReference = sourceModel.fields.find((f) => f.name === sourceNameFromReference); + + // Determine the relation field name: + // - For ambiguous relations (multiple FKs to the same table), include the source column for disambiguation. + // - Otherwise, prefer the name derived from the FK field (if no clash), falling back to the target model name. + let { name: sourceFieldName } = resolveNameCasing( + options.fieldCasing, + similarRelations > 0 + ? `${fieldPrefix}${lowerCaseFirst(sourceModel.name)}_${firstColumn}` + : `${(!sourceFieldFromReference? sourceNameFromReference : undefined) || lowerCaseFirst(resolveNameCasing(options.fieldCasing, targetModel.name).name)}${relation.type === 'many'? 's' : ''}`, + ); + + if (sourceModel.fields.find((f) => f.name === sourceFieldName)) { + sourceFieldName = `${sourceFieldName}To${lowerCaseFirst(targetModel.name)}_${relation.references.columns[0]}`; + } + + const sourceFieldFactory = new DataFieldFactory() + .setContainer(sourceModel) + .setName(sourceFieldName) + .setType((tb) => + tb + .setOptional(relation.nullable) + .setArray(relation.type === 'many') + .setReference(targetModel), + ); + sourceFieldFactory.addAttribute((ab) => { + ab.setDecl(relationAttribute); + if (includeRelationName) ab.addArg((ab) => ab.StringLiteral.setValue(relationName)); + + // Build fields array (all source FK columns) + ab.addArg((ab) => { + const arrayExpr = ab.ArrayExpr; + for (const { field } of sourceFields) { + arrayExpr.addItem((aeb) => aeb.ReferenceExpr.setTarget(field)); + } + return arrayExpr; + }, 'fields'); + + // Build references array (all target columns) + ab.addArg((ab) => { + const arrayExpr = ab.ArrayExpr; + for (const field of targetFields) { + arrayExpr.addItem((aeb) => aeb.ReferenceExpr.setTarget(field)); + } + return arrayExpr; + }, 'references'); + + // Prisma defaults: onDelete is SetNull for optional, Restrict for mandatory + const onDeleteDefault = relation.nullable ? 'SET NULL' : 'RESTRICT'; + if (relation.foreign_key_on_delete && relation.foreign_key_on_delete !== onDeleteDefault) { + const enumRef = getEnumRef('ReferentialAction', services); + if (!enumRef) throw new CliError('ReferentialAction enum not found'); + const enumFieldRef = enumRef.fields.find( + (f) => f.name.toLowerCase() === relation.foreign_key_on_delete!.replace(/ /g, '').toLowerCase(), + ); + if (!enumFieldRef) throw new CliError(`ReferentialAction ${relation.foreign_key_on_delete} not found`); + ab.addArg((a) => a.ReferenceExpr.setTarget(enumFieldRef), 'onDelete'); + } + + // Prisma default: onUpdate is Cascade + if (relation.foreign_key_on_update && relation.foreign_key_on_update !== 'CASCADE') { + const enumRef = getEnumRef('ReferentialAction', services); + if (!enumRef) throw new CliError('ReferentialAction enum not found'); + const enumFieldRef = enumRef.fields.find( + (f) => f.name.toLowerCase() === relation.foreign_key_on_update!.replace(/ /g, '').toLowerCase(), + ); + if (!enumFieldRef) throw new CliError(`ReferentialAction ${relation.foreign_key_on_update} not found`); + ab.addArg((a) => a.ReferenceExpr.setTarget(enumFieldRef), 'onUpdate'); + } + + // Check if the FK constraint name differs from the default pattern + const defaultFkName = `${relation.table}_${relation.columns.join('_')}_fkey`; + if (relation.fk_name && relation.fk_name !== defaultFkName) ab.addArg((ab) => ab.StringLiteral.setValue(relation.fk_name), 'map'); + + return ab; + }); + + sourceModel.fields.splice(firstSourceFieldId, 0, sourceFieldFactory.node); // Insert the relation field before the first FK scalar field + + const oppositeFieldPrefix = /[0-9]/g.test(targetModel.name.charAt(0)) ? '_' : ''; + const { name: oppositeFieldName } = resolveNameCasing( + options.fieldCasing, + similarRelations > 0 + ? `${oppositeFieldPrefix}${lowerCaseFirst(sourceModel.name)}_${firstColumn}` + : `${lowerCaseFirst(resolveNameCasing(options.fieldCasing, sourceModel.name).name)}${relation.references.type === 'many'? 's' : ''}`, + ); + + const targetFieldFactory = new DataFieldFactory() + .setContainer(targetModel) + .setName(oppositeFieldName) + .setType((tb) => + tb + .setOptional(relation.references.type === 'one') + .setArray(relation.references.type === 'many') + .setReference(sourceModel), + ); + if (includeRelationName) + targetFieldFactory.addAttribute((ab) => + ab.setDecl(relationAttribute).addArg((ab) => ab.StringLiteral.setValue(relationName)), + ); + + targetModel.fields.push(targetFieldFactory.node); +} diff --git a/packages/cli/src/actions/pull/provider/index.ts b/packages/cli/src/actions/pull/provider/index.ts new file mode 100644 index 000000000..7c93746d4 --- /dev/null +++ b/packages/cli/src/actions/pull/provider/index.ts @@ -0,0 +1,13 @@ +import type { DataSourceProviderType } from '@zenstackhq/schema'; +export * from './provider'; + +import { mysql } from './mysql'; +import { postgresql } from './postgresql'; +import type { IntrospectionProvider } from './provider'; +import { sqlite } from './sqlite'; + +export const providers: Record = { + mysql, + postgresql, + sqlite, +}; diff --git a/packages/cli/src/actions/pull/provider/mysql.ts b/packages/cli/src/actions/pull/provider/mysql.ts new file mode 100644 index 000000000..895a9cb53 --- /dev/null +++ b/packages/cli/src/actions/pull/provider/mysql.ts @@ -0,0 +1,597 @@ +import type { Attribute, BuiltinType } from '@zenstackhq/language/ast'; +import { DataFieldAttributeFactory } from '@zenstackhq/language/factory'; +import { getAttributeRef, getDbName, getFunctionRef, normalizeDecimalDefault, normalizeFloatDefault } from '../utils'; +import type { IntrospectedEnum, IntrospectedSchema, IntrospectedTable, IntrospectionProvider } from './provider'; +import { CliError } from '../../../cli-error'; +import { resolveNameCasing } from '../casing'; + +// Note: We dynamically import mysql2 inside the async function to avoid +// requiring it at module load time for environments that don't use MySQL. + +function normalizeGenerationExpression(typeDef: string): string { + // MySQL may include character set introducers in generation expressions, e.g. `_utf8mb4' '`. + // Strip them to produce a stable, cleaner expression for `Unsupported("...")`. + // MySQL commonly returns generation expressions with SQL-style quote escaping (e.g. `\\'`), + // which would become an invalid ZModel string after the code generator escapes quotes again. + // Normalize it to raw quotes, letting the ZModel code generator re-escape appropriately. + return ( + typeDef + // Remove character set introducers, with or without escaped quotes. + .replace(/_([0-9A-Za-z_]+)\\?'/g, "'") + // Unescape SQL-style escaped single quotes in the expression. + .replace(/\\'/g, "'") + ); +} + +export const mysql: IntrospectionProvider = { + isSupportedFeature(feature) { + switch (feature) { + case 'NativeEnum': + return true; + case 'Schema': + default: + return false; + } + }, + getBuiltinType(type) { + const t = (type || '').toLowerCase().trim(); + + // MySQL doesn't have native array types + const isArray = false; + + switch (t) { + // integers + case 'tinyint': + case 'smallint': + case 'mediumint': + case 'int': + case 'integer': + return { type: 'Int', isArray }; + case 'bigint': + return { type: 'BigInt', isArray }; + + // decimals and floats + case 'decimal': + case 'numeric': + return { type: 'Decimal', isArray }; + case 'float': + case 'double': + case 'real': + return { type: 'Float', isArray }; + + // boolean (MySQL uses TINYINT(1) for boolean) + case 'boolean': + case 'bool': + return { type: 'Boolean', isArray }; + + // strings + case 'char': + case 'varchar': + case 'tinytext': + case 'text': + case 'mediumtext': + case 'longtext': + return { type: 'String', isArray }; + + // dates/times + case 'date': + case 'time': + case 'datetime': + case 'timestamp': + case 'year': + return { type: 'DateTime', isArray }; + + // binary + case 'binary': + case 'varbinary': + case 'tinyblob': + case 'blob': + case 'mediumblob': + case 'longblob': + return { type: 'Bytes', isArray }; + + // json + case 'json': + return { type: 'Json', isArray }; + + default: + // Handle ENUM type - MySQL returns enum values like "enum('val1','val2')" + if (t.startsWith('enum(')) { + return { type: 'String', isArray }; + } + // Handle SET type + if (t.startsWith('set(')) { + return { type: 'String', isArray }; + } + return { type: 'Unsupported' as const, isArray }; + } + }, + getDefaultDatabaseType(type: BuiltinType) { + switch (type) { + case 'String': + return { type: 'varchar', precision: 191 }; + case 'Boolean': + // Boolean maps to 'boolean' (our synthetic type from tinyint(1)) + // No precision needed since we handle the mapping in the query + return { type: 'boolean' }; + case 'Int': + return { type: 'int' }; + case 'BigInt': + return { type: 'bigint' }; + case 'Float': + return { type: 'double' }; + case 'Decimal': + return { type: 'decimal', precision: 65 }; + case 'DateTime': + return { type: 'datetime', precision: 3 }; + case 'Json': + return { type: 'json' }; + case 'Bytes': + return { type: 'longblob' }; + } + }, + async introspect(connectionString: string, options: { schemas: string[]; modelCasing: 'pascal' | 'camel' | 'snake' | 'none' }): Promise { + const mysql = await import('mysql2/promise'); + const connection = await mysql.createConnection(connectionString); + + try { + // Extract database name from connection string + const url = new URL(connectionString); + const databaseName = url.pathname.replace('/', ''); + + if (!databaseName) { + throw new CliError('Database name not found in connection string'); + } + + // Introspect tables + const [tableRows] = (await connection.execute(getTableIntrospectionQuery(), [databaseName])) as [ + IntrospectedTable[], + unknown, + ]; + const tables: IntrospectedTable[] = []; + + for (const row of tableRows) { + const columns = typeof row.columns === 'string' ? JSON.parse(row.columns) : row.columns; + const indexes = typeof row.indexes === 'string' ? JSON.parse(row.indexes) : row.indexes; + + // Sort columns by ordinal_position to preserve database column order + const sortedColumns = (columns || []) + .sort( + (a: { ordinal_position?: number }, b: { ordinal_position?: number }) => + (a.ordinal_position ?? 0) - (b.ordinal_position ?? 0) + ) + .map((col: any) => { + // MySQL enum datatype_name is synthetic (TableName_ColumnName). + // Apply model casing so it matches the cased enum_type. + if (col.datatype === 'enum' && col.datatype_name) { + return { ...col, datatype_name: resolveNameCasing(options.modelCasing, col.datatype_name).name }; + } + // Normalize generated column expressions for stable output. + if (col.computed && typeof col.datatype === 'string') { + return { ...col, datatype: normalizeGenerationExpression(col.datatype) }; + } + return col; + }); + + // Filter out auto-generated FK indexes (MySQL creates these automatically) + // Pattern: {Table}_{column}_fkey for single-column FK indexes + const filteredIndexes = (indexes || []).filter( + (idx: { name: string; columns: { name: string }[] }) => + !(idx.columns.length === 1 && idx.name === `${row.name}_${idx.columns[0]?.name}_fkey`) + ); + + tables.push({ + schema: '', // MySQL doesn't support multi-schema + name: row.name, + type: row.type as 'table' | 'view', + definition: row.definition, + columns: sortedColumns, + indexes: filteredIndexes, + }); + } + + // Introspect enums (MySQL stores enum values in column definitions) + const [enumRows] = (await connection.execute(getEnumIntrospectionQuery(), [databaseName])) as [ + { table_name: string; column_name: string; column_type: string }[], + unknown, + ]; + + const enums: IntrospectedEnum[] = enumRows.map((row) => { + // Parse enum values from column_type like "enum('val1','val2','val3')" + const values = parseEnumValues(row.column_type); + // MySQL doesn't have standalone enum types; the name is entirely + // synthetic (TableName_ColumnName). Apply model casing here so it + // arrives already cased — there is no raw DB name to @@map back to. + const syntheticName = `${row.table_name}_${row.column_name}`; + const { name } = resolveNameCasing(options.modelCasing, syntheticName); + return { + schema_name: '', // MySQL doesn't support multi-schema + enum_type: name, + values, + }; + }); + + return { tables, enums }; + } finally { + await connection.end(); + } + }, + getDefaultValue({ defaultValue, fieldType, datatype, datatype_name, services, enums }) { + const val = defaultValue.trim(); + + // Handle NULL early + if (val.toUpperCase() === 'NULL') { + return null; + } + + // Handle enum defaults + if (datatype === 'enum' && datatype_name) { + const enumDef = enums.find((e) => getDbName(e) === datatype_name); + if (enumDef) { + // Strip quotes from the value (MySQL returns 'value') + const enumValue = val.startsWith("'") && val.endsWith("'") ? val.slice(1, -1) : val; + const enumField = enumDef.fields.find((f) => getDbName(f) === enumValue); + if (enumField) { + return (ab) => ab.ReferenceExpr.setTarget(enumField); + } + } + } + + switch (fieldType) { + case 'DateTime': + if (/^CURRENT_TIMESTAMP(\(\d*\))?$/i.test(val) || val.toLowerCase() === 'current_timestamp()' || val.toLowerCase() === 'now()') { + return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('now', services)); + } + // Fallback to string literal for other DateTime defaults + return (ab) => ab.StringLiteral.setValue(val); + + case 'Int': + case 'BigInt': + if (val.toLowerCase() === 'auto_increment') { + return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('autoincrement', services)); + } + return (ab) => ab.NumberLiteral.setValue(val); + + case 'Float': + return normalizeFloatDefault(val); + + case 'Decimal': + return normalizeDecimalDefault(val); + + case 'Boolean': + return (ab) => ab.BooleanLiteral.setValue(val.toLowerCase() === 'true' || val === '1' || val === "b'1'"); + + case 'String': + if (val.toLowerCase() === 'uuid()') { + return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('uuid', services)); + } + return (ab) => ab.StringLiteral.setValue(val); + } + + // Handle function calls (e.g., uuid(), now()) + if (val.includes('(') && val.includes(')')) { + return (ab) => + ab.InvocationExpr.setFunction(getFunctionRef('dbgenerated', services)).addArg((a) => + a.setValue((v) => v.StringLiteral.setValue(val)), + ); + } + + console.warn(`Unsupported default value type: "${defaultValue}" for field type "${fieldType}". Skipping default value.`); + return null; + }, + + getFieldAttributes({ fieldName, fieldType, datatype, length, precision, services }) { + const factories: DataFieldAttributeFactory[] = []; + + // Add @updatedAt for DateTime fields named updatedAt or updated_at + if (fieldType === 'DateTime' && (fieldName.toLowerCase() === 'updatedat' || fieldName.toLowerCase() === 'updated_at')) { + factories.push(new DataFieldAttributeFactory().setDecl(getAttributeRef('@updatedAt', services))); + } + + // Add @db.* attribute if the datatype differs from the default + const dbAttr = services.shared.workspace.IndexManager.allElements('Attribute').find( + (d) => d.name.toLowerCase() === `@db.${datatype.toLowerCase()}`, + )?.node as Attribute | undefined; + + const defaultDatabaseType = this.getDefaultDatabaseType(fieldType as BuiltinType); + + if ( + dbAttr && + defaultDatabaseType && + (defaultDatabaseType.type !== datatype || + (defaultDatabaseType.precision && + defaultDatabaseType.precision !== (length ?? precision))) + ) { + const dbAttrFactory = new DataFieldAttributeFactory().setDecl(dbAttr); + const sizeValue = length ?? precision; + if (sizeValue !== undefined && sizeValue !== null) { + dbAttrFactory.addArg((a) => a.NumberLiteral.setValue(sizeValue)); + } + factories.push(dbAttrFactory); + } + + return factories; + }, +}; + +function getTableIntrospectionQuery() { + // Note: We use subqueries with ORDER BY before JSON_ARRAYAGG to ensure ordering + // since MySQL < 8.0.21 doesn't support ORDER BY inside JSON_ARRAYAGG. + // MySQL doesn't support multi-schema, so we don't include schema in the result. + return ` +-- Main query: one row per table/view with columns and indexes as nested JSON arrays. +-- Uses INFORMATION_SCHEMA which is MySQL's standard metadata catalog. +SELECT + t.TABLE_NAME AS \`name\`, -- table or view name + CASE t.TABLE_TYPE -- map MySQL table type strings to our internal types + WHEN 'BASE TABLE' THEN 'table' + WHEN 'VIEW' THEN 'view' + ELSE NULL + END AS \`type\`, + CASE -- for views, retrieve the SQL definition + WHEN t.TABLE_TYPE = 'VIEW' THEN v.VIEW_DEFINITION + ELSE NULL + END AS \`definition\`, + + -- ===== COLUMNS subquery ===== + -- Wraps an ordered subquery in JSON_ARRAYAGG to produce a JSON array of column objects. + ( + SELECT JSON_ARRAYAGG(col_json) + FROM ( + SELECT JSON_OBJECT( + 'ordinal_position', c.ORDINAL_POSITION, -- column position (used for sorting) + 'name', c.COLUMN_NAME, -- column name + + -- datatype: for generated/computed columns, construct the full DDL-like type definition + -- (e.g., "int GENERATED ALWAYS AS (col1 + col2) STORED") so it can be rendered as + -- Unsupported("..."); special-case tinyint(1) as 'boolean' (MySQL's boolean convention); + -- otherwise use the DATA_TYPE (e.g., 'int', 'varchar', 'datetime'). + 'datatype', CASE + WHEN c.GENERATION_EXPRESSION IS NOT NULL AND c.GENERATION_EXPRESSION != '' THEN + CONCAT( + c.COLUMN_TYPE, + ' GENERATED ALWAYS AS (', + c.GENERATION_EXPRESSION, + ') ', + CASE + WHEN c.EXTRA LIKE '%STORED GENERATED%' THEN 'STORED' + ELSE 'VIRTUAL' + END + ) + WHEN c.DATA_TYPE = 'tinyint' AND c.COLUMN_TYPE = 'tinyint(1)' THEN 'boolean' + ELSE c.DATA_TYPE + END, + + -- datatype_name: for enum columns, generate a synthetic name "TableName_ColumnName" + -- (MySQL doesn't have named enum types like PostgreSQL) + 'datatype_name', CASE + WHEN c.DATA_TYPE = 'enum' THEN CONCAT(t.TABLE_NAME, '_', c.COLUMN_NAME) + ELSE NULL + END, + + 'datatype_schema', '', -- MySQL doesn't support multi-schema + 'length', c.CHARACTER_MAXIMUM_LENGTH, -- max length for string types (e.g., VARCHAR(255) -> 255) + 'precision', COALESCE(c.NUMERIC_PRECISION, c.DATETIME_PRECISION), -- numeric or datetime precision + + 'nullable', c.IS_NULLABLE = 'YES', -- true if column allows NULL + + -- default: for auto_increment columns, report 'auto_increment' instead of NULL; + -- otherwise use the COLUMN_DEFAULT value + 'default', CASE + WHEN c.EXTRA LIKE '%auto_increment%' THEN 'auto_increment' + ELSE c.COLUMN_DEFAULT + END, + + 'pk', c.COLUMN_KEY = 'PRI', -- true if column is part of the primary key + + -- unique: true if the column has a single-column unique index. + -- COLUMN_KEY = 'UNI' covers most cases, but may not be set when the column + -- also participates in other indexes (showing 'MUL' instead on some MySQL versions). + -- Also check INFORMATION_SCHEMA.STATISTICS for single-column unique indexes + -- (NON_UNIQUE = 0) to match the PostgreSQL introspection behavior. + 'unique', ( + c.COLUMN_KEY = 'UNI' + OR EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS s_uni + WHERE s_uni.TABLE_SCHEMA = c.TABLE_SCHEMA + AND s_uni.TABLE_NAME = c.TABLE_NAME + AND s_uni.COLUMN_NAME = c.COLUMN_NAME + AND s_uni.NON_UNIQUE = 0 + AND s_uni.INDEX_NAME != 'PRIMARY' + AND ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.STATISTICS s_cnt + WHERE s_cnt.TABLE_SCHEMA = s_uni.TABLE_SCHEMA + AND s_cnt.TABLE_NAME = s_uni.TABLE_NAME + AND s_cnt.INDEX_NAME = s_uni.INDEX_NAME + ) = 1 + ) + ), + 'unique_name', ( + SELECT COALESCE( + CASE WHEN c.COLUMN_KEY = 'UNI' THEN c.COLUMN_NAME ELSE NULL END, + ( + SELECT s_uni.INDEX_NAME + FROM INFORMATION_SCHEMA.STATISTICS s_uni + WHERE s_uni.TABLE_SCHEMA = c.TABLE_SCHEMA + AND s_uni.TABLE_NAME = c.TABLE_NAME + AND s_uni.COLUMN_NAME = c.COLUMN_NAME + AND s_uni.NON_UNIQUE = 0 + AND s_uni.INDEX_NAME != 'PRIMARY' + AND ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.STATISTICS s_cnt + WHERE s_cnt.TABLE_SCHEMA = s_uni.TABLE_SCHEMA + AND s_cnt.TABLE_NAME = s_uni.TABLE_NAME + AND s_cnt.INDEX_NAME = s_uni.INDEX_NAME + ) = 1 + LIMIT 1 + ) + ) + ), + + -- computed: true if column has a generation expression (virtual or stored) + 'computed', c.GENERATION_EXPRESSION IS NOT NULL AND c.GENERATION_EXPRESSION != '', + + -- options: for enum columns, the full COLUMN_TYPE string (e.g., "enum('a','b','c')") + -- which gets parsed into individual values later + 'options', CASE + WHEN c.DATA_TYPE = 'enum' THEN c.COLUMN_TYPE + ELSE NULL + END, + + -- Foreign key info (NULL if column is not part of a FK) + 'foreign_key_schema', NULL, -- MySQL doesn't support cross-schema FKs here + 'foreign_key_table', kcu_fk.REFERENCED_TABLE_NAME, -- referenced table + 'foreign_key_column', kcu_fk.REFERENCED_COLUMN_NAME, -- referenced column + 'foreign_key_name', kcu_fk.CONSTRAINT_NAME, -- FK constraint name + 'foreign_key_on_update', rc.UPDATE_RULE, -- referential action on update (CASCADE, SET NULL, etc.) + 'foreign_key_on_delete', rc.DELETE_RULE -- referential action on delete + ) AS col_json + + FROM INFORMATION_SCHEMA.COLUMNS c -- one row per column in the database + + -- Join KEY_COLUMN_USAGE to find foreign key references for this column. + -- Filter to only FK entries (REFERENCED_TABLE_NAME IS NOT NULL). + LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu_fk + ON c.TABLE_SCHEMA = kcu_fk.TABLE_SCHEMA + AND c.TABLE_NAME = kcu_fk.TABLE_NAME + AND c.COLUMN_NAME = kcu_fk.COLUMN_NAME + AND kcu_fk.REFERENCED_TABLE_NAME IS NOT NULL + + -- Join REFERENTIAL_CONSTRAINTS to get ON UPDATE / ON DELETE rules for the FK. + LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc + ON kcu_fk.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA + AND kcu_fk.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + + WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA + AND c.TABLE_NAME = t.TABLE_NAME + ORDER BY c.ORDINAL_POSITION -- preserve original column order + ) AS cols_ordered + ) AS \`columns\`, + + -- ===== INDEXES subquery ===== + -- Aggregates all indexes for this table into a JSON array. + ( + SELECT JSON_ARRAYAGG(idx_json) + FROM ( + SELECT JSON_OBJECT( + 'name', s.INDEX_NAME, -- index name (e.g., 'PRIMARY', 'idx_email') + 'method', s.INDEX_TYPE, -- index type (e.g., 'BTREE', 'HASH', 'FULLTEXT') + 'unique', s.NON_UNIQUE = 0, -- NON_UNIQUE=0 means it IS unique + 'primary', s.INDEX_NAME = 'PRIMARY', -- MySQL names the PK index 'PRIMARY' + 'valid', TRUE, -- MySQL doesn't expose index validity status + 'ready', TRUE, -- MySQL doesn't expose index readiness status + 'partial', FALSE, -- MySQL doesn't support partial indexes + 'predicate', NULL, -- no WHERE clause on indexes in MySQL + + -- Index columns: nested subquery for columns in this index + 'columns', ( + SELECT JSON_ARRAYAGG(idx_col_json) + FROM ( + SELECT JSON_OBJECT( + 'name', s2.COLUMN_NAME, -- column name in the index + 'expression', NULL, -- MySQL doesn't expose expression indexes via STATISTICS + -- COLLATION: 'A' = ascending, 'D' = descending, NULL = not sorted + 'order', CASE s2.COLLATION WHEN 'A' THEN 'ASC' WHEN 'D' THEN 'DESC' ELSE NULL END, + 'nulls', NULL -- MySQL doesn't expose NULLS FIRST/LAST + ) AS idx_col_json + FROM INFORMATION_SCHEMA.STATISTICS s2 -- one row per column per index + WHERE s2.TABLE_SCHEMA = s.TABLE_SCHEMA + AND s2.TABLE_NAME = s.TABLE_NAME + AND s2.INDEX_NAME = s.INDEX_NAME + ORDER BY s2.SEQ_IN_INDEX -- preserve column order within the index + ) AS idx_cols_ordered + ) + ) AS idx_json + FROM ( + -- Deduplicate: STATISTICS has one row per (index, column), but we need one row per index. + -- DISTINCT on INDEX_NAME gives us one entry per index with its metadata. + SELECT DISTINCT INDEX_NAME, INDEX_TYPE, NON_UNIQUE, TABLE_SCHEMA, TABLE_NAME + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = t.TABLE_SCHEMA AND TABLE_NAME = t.TABLE_NAME + ) s + ) AS idxs_ordered + ) AS \`indexes\` + +-- === Main FROM: INFORMATION_SCHEMA.TABLES lists all tables and views === +FROM INFORMATION_SCHEMA.TABLES t +-- Join VIEWS to get VIEW_DEFINITION for view tables +LEFT JOIN INFORMATION_SCHEMA.VIEWS v + ON t.TABLE_SCHEMA = v.TABLE_SCHEMA AND t.TABLE_NAME = v.TABLE_NAME +WHERE t.TABLE_SCHEMA = ? -- only the target database + AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW') -- exclude system tables like SYSTEM VIEW + AND t.TABLE_NAME <> '_prisma_migrations' -- exclude Prisma migration tracking table +ORDER BY t.TABLE_NAME; +`; +} + +function getEnumIntrospectionQuery() { + // MySQL doesn't have standalone enum types like PostgreSQL's CREATE TYPE. + // Instead, enum values are embedded in column definitions (e.g., COLUMN_TYPE = "enum('a','b','c')"). + // This query finds all enum columns so we can extract their allowed values. + return ` +SELECT + c.TABLE_NAME AS table_name, -- table containing the enum column + c.COLUMN_NAME AS column_name, -- column name + c.COLUMN_TYPE AS column_type -- full type string including values (e.g., "enum('val1','val2')") +FROM INFORMATION_SCHEMA.COLUMNS c +WHERE c.TABLE_SCHEMA = ? -- only the target database + AND c.DATA_TYPE = 'enum' -- only enum columns +ORDER BY c.TABLE_NAME, c.COLUMN_NAME; +`; +} + +/** + * Parse enum values from MySQL COLUMN_TYPE string like "enum('val1','val2','val3')" + */ +function parseEnumValues(columnType: string): string[] { + // Match the content inside enum(...) + const match = columnType.match(/^enum\((.+)\)$/i); + if (!match || !match[1]) return []; + + const valuesString = match[1]; + const values: string[] = []; + + // Parse quoted values, handling escaped quotes + let current = ''; + let inQuote = false; + let i = 0; + + while (i < valuesString.length) { + const char = valuesString[i]; + + if (char === "'" && !inQuote) { + inQuote = true; + i++; + continue; + } + + if (char === "'" && inQuote) { + // Check for escaped quote ('') + if (valuesString[i + 1] === "'") { + current += "'"; + i += 2; + continue; + } + // End of value + values.push(current); + current = ''; + inQuote = false; + i++; + // Skip comma and any whitespace + while (i < valuesString.length && (valuesString[i] === ',' || valuesString[i] === ' ')) { + i++; + } + continue; + } + + if (inQuote) { + current += char; + } + i++; + } + + return values; +} diff --git a/packages/cli/src/actions/pull/provider/postgresql.ts b/packages/cli/src/actions/pull/provider/postgresql.ts new file mode 100644 index 000000000..bf54e5658 --- /dev/null +++ b/packages/cli/src/actions/pull/provider/postgresql.ts @@ -0,0 +1,653 @@ +import type { Attribute, BuiltinType, Enum, Expression } from '@zenstackhq/language/ast'; +import { AstFactory, DataFieldAttributeFactory, ExpressionBuilder } from '@zenstackhq/language/factory'; +import { Client } from 'pg'; +import { getAttributeRef, getDbName, getFunctionRef, normalizeDecimalDefault, normalizeFloatDefault } from '../utils'; +import type { IntrospectedEnum, IntrospectedSchema, IntrospectedTable, IntrospectionProvider } from './provider'; +import type { ZModelServices } from '@zenstackhq/language'; +import { CliError } from '../../../cli-error'; + +/** + * Maps PostgreSQL internal type names to their standard SQL names for comparison. + * This is used to normalize type names when checking against default database types. + */ +const pgTypnameToStandard: Record = { + int2: 'smallint', + int4: 'integer', + int8: 'bigint', + float4: 'real', + float8: 'double precision', + bool: 'boolean', + bpchar: 'character', + numeric: 'decimal', +}; + +/** + * Standard bit widths for integer/float types that shouldn't be added as precision arguments. + * PostgreSQL returns these as precision values, but they're implicit for the type. + */ +const standardTypePrecisions: Record = { + int2: 16, + smallint: 16, + int4: 32, + integer: 32, + int8: 64, + bigint: 64, + float4: 24, + real: 24, + float8: 53, + 'double precision': 53, +}; + +/** + * Maps PostgreSQL typnames (from pg_type.typname) to ZenStack native type attribute names. + * PostgreSQL introspection returns internal type names like 'int2', 'int4', 'float8', 'bpchar', + * but ZenStack attributes are named @db.SmallInt, @db.Integer, @db.DoublePrecision, @db.Char, etc. + */ +const pgTypnameToZenStackNativeType: Record = { + // integers + int2: 'SmallInt', + smallint: 'SmallInt', + int4: 'Integer', + integer: 'Integer', + int8: 'BigInt', + bigint: 'BigInt', + + // decimals and floats + numeric: 'Decimal', + decimal: 'Decimal', + float4: 'Real', + real: 'Real', + float8: 'DoublePrecision', + 'double precision': 'DoublePrecision', + + // boolean + bool: 'Boolean', + boolean: 'Boolean', + + // strings + text: 'Text', + varchar: 'VarChar', + 'character varying': 'VarChar', + bpchar: 'Char', + character: 'Char', + + // uuid + uuid: 'Uuid', + + // dates/times + date: 'Date', + time: 'Time', + timetz: 'Timetz', + timestamp: 'Timestamp', + timestamptz: 'Timestamptz', + + // binary + bytea: 'ByteA', + + // json + json: 'Json', + jsonb: 'JsonB', + + // xml + xml: 'Xml', + + // network types + inet: 'Inet', + + // bit strings + bit: 'Bit', + varbit: 'VarBit', + + // oid + oid: 'Oid', + + // money + money: 'Money', + + // citext extension + citext: 'Citext', +}; + +export const postgresql: IntrospectionProvider = { + isSupportedFeature(feature) { + const supportedFeatures = ['Schema', 'NativeEnum']; + return supportedFeatures.includes(feature); + }, + getBuiltinType(type) { + const t = (type || '').toLowerCase(); + + const isArray = t.startsWith('_'); + + switch (t.replace(/^_/, '')) { + // integers + case 'int2': + case 'smallint': + case 'int4': + case 'integer': + return { type: 'Int', isArray }; + case 'int8': + case 'bigint': + return { type: 'BigInt', isArray }; + + // decimals and floats + case 'numeric': + case 'decimal': + return { type: 'Decimal', isArray }; + case 'float4': + case 'real': + case 'float8': + case 'double precision': + return { type: 'Float', isArray }; + + // boolean + case 'bool': + case 'boolean': + return { type: 'Boolean', isArray }; + + // strings + case 'text': + case 'varchar': + case 'bpchar': + case 'character varying': + case 'character': + return { type: 'String', isArray }; + + // uuid + case 'uuid': + return { type: 'String', isArray }; + + // dates/times + case 'date': + case 'time': + case 'timetz': + case 'timestamp': + case 'timestamptz': + return { type: 'DateTime', isArray }; + + // binary + case 'bytea': + return { type: 'Bytes', isArray }; + + // json + case 'json': + case 'jsonb': + return { type: 'Json', isArray }; + default: + return { type: 'Unsupported' as const, isArray }; + } + }, + async introspect(connectionString: string, options: { schemas: string[]; modelCasing: 'pascal' | 'camel' | 'snake' | 'none' }): Promise { + const client = new Client({ connectionString }); + await client.connect(); + + try { + const { rows: tables } = await client.query(tableIntrospectionQuery); + const { rows: enums } = await client.query(enumIntrospectionQuery); + + // Filter tables and enums to only include those from the selected schemas + const filteredTables = tables.filter((t) => options.schemas.includes(t.schema)); + const filteredEnums = enums.filter((e) => options.schemas.includes(e.schema_name)); + + return { + enums: filteredEnums, + tables: filteredTables, + }; + } finally { + await client.end(); + } + }, + getDefaultDatabaseType(type: BuiltinType) { + switch (type) { + case 'String': + return { type: 'text' }; + case 'Boolean': + return { type: 'boolean' }; + case 'Int': + return { type: 'integer' }; + case 'BigInt': + return { type: 'bigint' }; + case 'Float': + return { type: 'double precision' }; + case 'Decimal': + return { type: 'decimal' }; + case 'DateTime': + return { type: 'timestamp', precision: 3 }; + case 'Json': + return { type: 'jsonb' }; + case 'Bytes': + return { type: 'bytea' }; + } + }, + getDefaultValue({ defaultValue, fieldType, datatype, datatype_name, services, enums }) { + const val = defaultValue.trim(); + + // Handle enum defaults (PostgreSQL returns 'value'::enum_type) + if (datatype === 'enum' && datatype_name) { + const enumDef = enums.find((e) => getDbName(e) === datatype_name); + if (enumDef) { + // Extract the enum value from the default (format: 'VALUE'::"enum_type") + const enumValue = val.replace(/'/g, '').split('::')[0]?.trim(); + const enumField = enumDef.fields.find((f) => getDbName(f) === enumValue); + if (enumField) { + return (ab) => ab.ReferenceExpr.setTarget(enumField); + } + } + // Fall through to typeCastingConvert if datatype_name lookup fails + return typeCastingConvert({defaultValue,enums,val,services}); + } + + switch (fieldType) { + case 'DateTime': + if (val === 'CURRENT_TIMESTAMP' || val === 'now()') { + return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('now', services)); + } + + if (val.includes('::')) { + return typeCastingConvert({defaultValue,enums,val,services}); + } + + // Fallback to string literal for other DateTime defaults + return (ab) => ab.StringLiteral.setValue(val); + + case 'Int': + case 'BigInt': + if (val.startsWith('nextval(')) { + return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('autoincrement', services)); + } + + if (val.includes('::')) { + return typeCastingConvert({defaultValue,enums,val,services}); + } + return (ab) => ab.NumberLiteral.setValue(val); + + case 'Float': + if (val.includes('::')) { + return typeCastingConvert({defaultValue,enums,val,services}); + } + return normalizeFloatDefault(val); + + case 'Decimal': + if (val.includes('::')) { + return typeCastingConvert({defaultValue,enums,val,services}); + } + return normalizeDecimalDefault(val); + + case 'Boolean': + return (ab) => ab.BooleanLiteral.setValue(val === 'true'); + + case 'String': + if (val.includes('::')) { + return typeCastingConvert({defaultValue,enums,val,services}); + } + + if (val.startsWith("'") && val.endsWith("'")) { + return (ab) => ab.StringLiteral.setValue(val.slice(1, -1).replace(/''/g, "'")); + } + return (ab) => ab.StringLiteral.setValue(val); + } + + if (val.includes('(') && val.includes(')')) { + return (ab) => + ab.InvocationExpr.setFunction(getFunctionRef('dbgenerated', services)).addArg((a) => + a.setValue((v) => v.StringLiteral.setValue(val)), + ); + } + + console.warn(`Unsupported default value type: "${defaultValue}" for field type "${fieldType}". Skipping default value.`); + return null; + }, + + getFieldAttributes({ fieldName, fieldType, datatype, length, precision, services }) { + const factories: DataFieldAttributeFactory[] = []; + + // Add @updatedAt for DateTime fields named updatedAt or updated_at + if (fieldType === 'DateTime' && (fieldName.toLowerCase() === 'updatedat' || fieldName.toLowerCase() === 'updated_at')) { + factories.push(new DataFieldAttributeFactory().setDecl(getAttributeRef('@updatedAt', services))); + } + + // Map PostgreSQL typname to ZenStack native type attribute name + // PostgreSQL returns typnames like 'int2', 'float8', 'bpchar', but ZenStack attributes + // are named @db.SmallInt, @db.DoublePrecision, @db.Char, etc. + const nativeTypeName = pgTypnameToZenStackNativeType[datatype.toLowerCase()] ?? datatype; + + // Add @db.* attribute if the datatype differs from the default + const dbAttr = services.shared.workspace.IndexManager.allElements('Attribute').find( + (d) => d.name.toLowerCase() === `@db.${nativeTypeName.toLowerCase()}`, + )?.node as Attribute | undefined; + + const defaultDatabaseType = this.getDefaultDatabaseType(fieldType as BuiltinType); + + // Normalize datatype for comparison (e.g., 'int4' -> 'integer') + const normalizedDatatype = pgTypnameToStandard[datatype.toLowerCase()] ?? datatype.toLowerCase(); + + // Check if the precision is the standard bit width for this type (shouldn't be added) + const standardPrecision = standardTypePrecisions[datatype.toLowerCase()]; + const isStandardPrecision = standardPrecision !== undefined && precision === standardPrecision; + + if ( + dbAttr && + defaultDatabaseType && + (defaultDatabaseType.type !== normalizedDatatype || + (defaultDatabaseType.precision && + defaultDatabaseType.precision !== (length ?? precision))) + ) { + const dbAttrFactory = new DataFieldAttributeFactory().setDecl(dbAttr); + // Only add length/precision if it's meaningful (not the standard bit width for the type) + if ((length || precision) && !isStandardPrecision) { + dbAttrFactory.addArg((a) => a.NumberLiteral.setValue(length! || precision!)); + } + factories.push(dbAttrFactory); + } + + return factories; + }, +}; + +const enumIntrospectionQuery = ` +SELECT + n.nspname AS schema_name, -- schema the enum belongs to (e.g., 'public') + t.typname AS enum_type, -- enum type name as defined in CREATE TYPE + coalesce(json_agg(e.enumlabel ORDER BY e.enumsortorder), '[]') AS values -- ordered list of enum labels as JSON array +FROM pg_type t -- pg_type: catalog of all data types +JOIN pg_enum e ON t.oid = e.enumtypid -- pg_enum: one row per enum label; join to get labels for this enum type +JOIN pg_namespace n ON n.oid = t.typnamespace -- pg_namespace: schema info; join to get the schema name +GROUP BY schema_name, enum_type -- one row per enum type, with all labels aggregated +ORDER BY schema_name, enum_type;`; + +const tableIntrospectionQuery = ` +-- Main query: one row per table/view with columns and indexes as nested JSON arrays. +-- Joins pg_class (tables/views) with pg_namespace (schemas). +SELECT + "ns"."nspname" AS "schema", -- schema name (e.g., 'public') + "cls"."relname" AS "name", -- table or view name + CASE "cls"."relkind" -- relkind: 'r' = ordinary table, 'v' = view + WHEN 'r' THEN 'table' + WHEN 'v' THEN 'view' + ELSE NULL + END AS "type", + CASE -- for views, retrieve the SQL definition + WHEN "cls"."relkind" = 'v' THEN pg_get_viewdef("cls"."oid", true) + ELSE NULL + END AS "definition", + + -- ===== COLUMNS subquery ===== + -- Aggregates all columns for this table into a JSON array. + ( + SELECT coalesce(json_agg(agg), '[]') + FROM ( + SELECT + "att"."attname" AS "name", -- column name + + -- datatype: if the type is an enum, report 'enum'; + -- if the column is generated/computed, construct the full DDL-like type definition + -- (e.g., "text GENERATED ALWAYS AS (expr) STORED") so it can be rendered as Unsupported("..."); + -- otherwise use the pg_type name. + CASE + WHEN EXISTS ( + SELECT 1 FROM "pg_catalog"."pg_enum" AS "e" + WHERE "e"."enumtypid" = "typ"."oid" + ) THEN 'enum' + WHEN "att"."attgenerated" != '' THEN + format_type("att"."atttypid", "att"."atttypmod") + || ' GENERATED ALWAYS AS (' + || pg_get_expr("def"."adbin", "def"."adrelid") + || ') ' + || CASE "att"."attgenerated" + WHEN 's' THEN 'STORED' + WHEN 'v' THEN 'VIRTUAL' + ELSE 'STORED' + END + ELSE "typ"."typname"::text -- internal type name (e.g., 'int4', 'varchar', 'text'); cast to text to prevent CASE from coercing result to name type (max 63 chars) + END AS "datatype", + + -- datatype_name: for enums only, the actual enum type name (used to look up the enum definition) + CASE + WHEN EXISTS ( + SELECT 1 FROM "pg_catalog"."pg_enum" AS "e" + WHERE "e"."enumtypid" = "typ"."oid" + ) THEN "typ"."typname" + ELSE NULL + END AS "datatype_name", + + "tns"."nspname" AS "datatype_schema", -- schema where the data type is defined + "c"."character_maximum_length" AS "length", -- max length for char/varchar types (from information_schema) + COALESCE("c"."numeric_precision", "c"."datetime_precision") AS "precision", -- numeric or datetime precision + + -- Foreign key info (NULL if column is not part of a FK constraint) + "fk_ns"."nspname" AS "foreign_key_schema", -- schema of the referenced table + "fk_cls"."relname" AS "foreign_key_table", -- referenced table name + "fk_att"."attname" AS "foreign_key_column", -- referenced column name + "fk_con"."conname" AS "foreign_key_name", -- FK constraint name + + -- FK referential actions: decode single-char codes to human-readable strings + CASE "fk_con"."confupdtype" + WHEN 'a' THEN 'NO ACTION' + WHEN 'r' THEN 'RESTRICT' + WHEN 'c' THEN 'CASCADE' + WHEN 'n' THEN 'SET NULL' + WHEN 'd' THEN 'SET DEFAULT' + ELSE NULL + END AS "foreign_key_on_update", + CASE "fk_con"."confdeltype" + WHEN 'a' THEN 'NO ACTION' + WHEN 'r' THEN 'RESTRICT' + WHEN 'c' THEN 'CASCADE' + WHEN 'n' THEN 'SET NULL' + WHEN 'd' THEN 'SET DEFAULT' + ELSE NULL + END AS "foreign_key_on_delete", + + -- pk: true if this column is part of the table's primary key constraint + "pk_con"."conkey" IS NOT NULL AS "pk", + + -- unique: true if the column has a single-column UNIQUE constraint OR a single-column unique index + ( + -- Check for a single-column UNIQUE constraint (contype = 'u') + EXISTS ( + SELECT 1 + FROM "pg_catalog"."pg_constraint" AS "u_con" + WHERE "u_con"."contype" = 'u' -- 'u' = unique constraint + AND "u_con"."conrelid" = "cls"."oid" -- on this table + AND array_length("u_con"."conkey", 1) = 1 -- single-column only + AND "att"."attnum" = ANY ("u_con"."conkey") -- this column is in the constraint + ) + OR + -- Check for a single-column unique index (may exist without an explicit constraint) + EXISTS ( + SELECT 1 + FROM "pg_catalog"."pg_index" AS "u_idx" + WHERE "u_idx"."indrelid" = "cls"."oid" -- on this table + AND "u_idx"."indisunique" = TRUE -- it's a unique index + AND "u_idx"."indnkeyatts" = 1 -- single key column + AND "att"."attnum" = ANY ("u_idx"."indkey"::int2[]) -- this column is the key + ) + ) AS "unique", + + -- unique_name: the name of the unique constraint or index (whichever exists first) + ( + SELECT COALESCE( + -- Try constraint name first + ( + SELECT "u_con"."conname" + FROM "pg_catalog"."pg_constraint" AS "u_con" + WHERE "u_con"."contype" = 'u' + AND "u_con"."conrelid" = "cls"."oid" + AND array_length("u_con"."conkey", 1) = 1 + AND "att"."attnum" = ANY ("u_con"."conkey") + LIMIT 1 + ), + -- Fall back to unique index name + ( + SELECT "u_idx_cls"."relname" + FROM "pg_catalog"."pg_index" AS "u_idx" + JOIN "pg_catalog"."pg_class" AS "u_idx_cls" ON "u_idx"."indexrelid" = "u_idx_cls"."oid" + WHERE "u_idx"."indrelid" = "cls"."oid" + AND "u_idx"."indisunique" = TRUE + AND "u_idx"."indnkeyatts" = 1 + AND "att"."attnum" = ANY ("u_idx"."indkey"::int2[]) + LIMIT 1 + ) + ) + ) AS "unique_name", + + "att"."attgenerated" != '' AS "computed", -- true if column is a generated/computed column + -- For generated columns, pg_attrdef stores the generation expression (not a default), + -- so we must null it out to avoid emitting a spurious @default(dbgenerated(...)) attribute. + CASE + WHEN "att"."attgenerated" != '' THEN NULL + ELSE pg_get_expr("def"."adbin", "def"."adrelid") + END AS "default", -- column default expression as text (e.g., 'nextval(...)', '0', 'now()') + "att"."attnotnull" != TRUE AS "nullable", -- true if column allows NULL values + + -- options: for enum columns, aggregates all allowed enum labels into a JSON array + coalesce( + ( + SELECT json_agg("enm"."enumlabel") AS "o" + FROM "pg_catalog"."pg_enum" AS "enm" + WHERE "enm"."enumtypid" = "typ"."oid" + ), + '[]' + ) AS "options" + + -- === FROM / JOINs for the columns subquery === + + -- pg_attribute: one row per table column (attnum >= 0 excludes system columns) + FROM "pg_catalog"."pg_attribute" AS "att" + + -- pg_type: data type of the column (e.g., int4, text, custom_enum) + INNER JOIN "pg_catalog"."pg_type" AS "typ" ON "typ"."oid" = "att"."atttypid" + + -- pg_namespace for the type: needed to determine which schema the type lives in + INNER JOIN "pg_catalog"."pg_namespace" AS "tns" ON "tns"."oid" = "typ"."typnamespace" + + -- information_schema.columns: provides length/precision info not easily available from pg_catalog + LEFT JOIN "information_schema"."columns" AS "c" ON "c"."table_schema" = "ns"."nspname" + AND "c"."table_name" = "cls"."relname" + AND "c"."column_name" = "att"."attname" + + -- pg_constraint (primary key): join on contype='p' to detect if column is part of PK + LEFT JOIN "pg_catalog"."pg_constraint" AS "pk_con" ON "pk_con"."contype" = 'p' + AND "pk_con"."conrelid" = "cls"."oid" + AND "att"."attnum" = ANY ("pk_con"."conkey") + + -- pg_constraint (foreign key): join on contype='f' to get FK details for this column + LEFT JOIN "pg_catalog"."pg_constraint" AS "fk_con" ON "fk_con"."contype" = 'f' + AND "fk_con"."conrelid" = "cls"."oid" + AND "att"."attnum" = ANY ("fk_con"."conkey") + + -- pg_class for FK target table: resolve the referenced table's OID to its name + LEFT JOIN "pg_catalog"."pg_class" AS "fk_cls" ON "fk_cls"."oid" = "fk_con"."confrelid" + + -- pg_namespace for FK target: get the schema of the referenced table + LEFT JOIN "pg_catalog"."pg_namespace" AS "fk_ns" ON "fk_ns"."oid" = "fk_cls"."relnamespace" + + -- pg_attribute for FK target column: resolve the referenced column number to its name. + -- Use array_position to correlate by position: find this source column's index in conkey, + -- then pick the referenced attnum at that same index from confkey. + -- This ensures composite FKs correctly map each source column to its corresponding target column. + LEFT JOIN "pg_catalog"."pg_attribute" AS "fk_att" ON "fk_att"."attrelid" = "fk_cls"."oid" + AND "fk_att"."attnum" = "fk_con"."confkey"[array_position("fk_con"."conkey", "att"."attnum")] + + -- pg_attrdef: column defaults; adbin contains the internal expression, decoded via pg_get_expr() + LEFT JOIN "pg_catalog"."pg_attrdef" AS "def" ON "def"."adrelid" = "cls"."oid" AND "def"."adnum" = "att"."attnum" + + WHERE + "att"."attrelid" = "cls"."oid" -- only columns belonging to this table + AND "att"."attnum" >= 0 -- exclude system columns (ctid, xmin, etc. have attnum < 0) + AND "att"."attisdropped" != TRUE -- exclude dropped (deleted) columns + ORDER BY "att"."attnum" -- preserve original column order + ) AS agg + ) AS "columns", + + -- ===== INDEXES subquery ===== + -- Aggregates all indexes for this table into a JSON array. + ( + SELECT coalesce(json_agg(agg), '[]') + FROM ( + SELECT + "idx_cls"."relname" AS "name", -- index name + "am"."amname" AS "method", -- access method (e.g., 'btree', 'hash', 'gin', 'gist') + "idx"."indisunique" AS "unique", -- true if unique index + "idx"."indisprimary" AS "primary", -- true if this is the PK index + "idx"."indisvalid" AS "valid", -- false during concurrent index builds + "idx"."indisready" AS "ready", -- true when index is ready for inserts + ("idx"."indpred" IS NOT NULL) AS "partial", -- true if index has a WHERE clause (partial index) + pg_get_expr("idx"."indpred", "idx"."indrelid") AS "predicate", -- the WHERE clause expression for partial indexes + + -- Index columns: iterate over each position in the index key array + ( + SELECT json_agg( + json_build_object( + -- 'name': column name, or for expression indexes the expression text + 'name', COALESCE("att"."attname", pg_get_indexdef("idx"."indexrelid", "s"."i", true)), + -- 'expression': non-null only for expression-based index columns (e.g., lower(name)) + 'expression', CASE WHEN "att"."attname" IS NULL THEN pg_get_indexdef("idx"."indexrelid", "s"."i", true) ELSE NULL END, + -- 'order': sort direction; bit 0 of indoption = 1 means DESC + 'order', CASE ((( "idx"."indoption"::int2[] )["s"."i"] & 1)) WHEN 1 THEN 'DESC' ELSE 'ASC' END, + -- 'nulls': null ordering; bit 1 of indoption = 1 means NULLS FIRST + 'nulls', CASE (((( "idx"."indoption"::int2[] )["s"."i"] >> 1) & 1)) WHEN 1 THEN 'NULLS FIRST' ELSE 'NULLS LAST' END + ) + ORDER BY "s"."i" -- preserve column order within the index + ) + -- generate_subscripts creates one row per index key position (1-based) + FROM generate_subscripts("idx"."indkey"::int2[], 1) AS "s"("i") + -- Join to pg_attribute to resolve column numbers to names + -- NULL attname means it's an expression index column + LEFT JOIN "pg_catalog"."pg_attribute" AS "att" + ON "att"."attrelid" = "cls"."oid" + AND "att"."attnum" = ("idx"."indkey"::int2[])["s"."i"] + ) AS "columns" + + FROM "pg_catalog"."pg_index" AS "idx" -- pg_index: one row per index + JOIN "pg_catalog"."pg_class" AS "idx_cls" ON "idx"."indexrelid" = "idx_cls"."oid" -- index's own pg_class entry (for the name) + JOIN "pg_catalog"."pg_am" AS "am" ON "idx_cls"."relam" = "am"."oid" -- access method catalog + WHERE "idx"."indrelid" = "cls"."oid" -- only indexes on this table + ORDER BY "idx_cls"."relname" + ) AS agg + ) AS "indexes" + +-- === Main FROM: pg_class (tables and views) joined with pg_namespace (schemas) === +FROM "pg_catalog"."pg_class" AS "cls" +INNER JOIN "pg_catalog"."pg_namespace" AS "ns" ON "cls"."relnamespace" = "ns"."oid" +WHERE + "ns"."nspname" !~ '^pg_' -- exclude PostgreSQL internal schemas (pg_catalog, pg_toast, etc.) + AND "ns"."nspname" != 'information_schema' -- exclude the information_schema + AND "cls"."relkind" IN ('r', 'v') -- only tables ('r') and views ('v') + AND "cls"."relname" !~ '^pg_' -- exclude system tables starting with pg_ + AND "cls"."relname" !~ '_prisma_migrations' -- exclude Prisma migration tracking table + ORDER BY "ns"."nspname", "cls"."relname" ASC; +`; + +function typeCastingConvert({defaultValue, enums, val, services}:{val: string, enums: Enum[], defaultValue:string, services:ZModelServices}): ((builder: ExpressionBuilder) => AstFactory) | null { + const [value, type] = val + .replace(/'/g, '') + .split('::') + .map((s) => s.trim()) as [string, string]; + switch (type) { + case 'character varying': + case 'uuid': + case 'json': + case 'jsonb': + case 'text': + if (value === 'NULL') return null; + return (ab) => ab.StringLiteral.setValue(value); + case 'real': + return (ab) => ab.NumberLiteral.setValue(value); + default: { + const enumDef = enums.find((e) => getDbName(e, true) === type); + if (!enumDef) { + return (ab) => + ab.InvocationExpr.setFunction(getFunctionRef('dbgenerated', services)).addArg((a) => + a.setValue((v) => v.StringLiteral.setValue(val)), + ); + } + const enumField = enumDef.fields.find((v) => getDbName(v) === value); + if (!enumField) { + throw new CliError( + `Enum value ${value} not found in enum ${type} for default value ${defaultValue}`, + ); + } + return (ab) => ab.ReferenceExpr.setTarget(enumField); + } + } +} diff --git a/packages/cli/src/actions/pull/provider/provider.ts b/packages/cli/src/actions/pull/provider/provider.ts new file mode 100644 index 000000000..7b3127132 --- /dev/null +++ b/packages/cli/src/actions/pull/provider/provider.ts @@ -0,0 +1,96 @@ +import type { ZModelServices } from '@zenstackhq/language'; +import type { BuiltinType, Enum, Expression } from '@zenstackhq/language/ast'; +import type { AstFactory, DataFieldAttributeFactory, ExpressionBuilder } from '@zenstackhq/language/factory'; + +export type Cascade = 'NO ACTION' | 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | null; + +export interface IntrospectedTable { + schema: string; + name: string; + type: 'table' | 'view'; + definition: string | null; + columns: { + name: string; + datatype: string; + datatype_name: string | null; + length: number | null; + precision: number | null; + datatype_schema: string; + foreign_key_schema: string | null; + foreign_key_table: string | null; + foreign_key_column: string | null; + foreign_key_name: string | null; + foreign_key_on_update: Cascade; + foreign_key_on_delete: Cascade; + pk: boolean; + computed: boolean; + nullable: boolean; + unique: boolean; + unique_name: string | null; + default: string | null; + }[]; + indexes: { + name: string; + method: string | null; + unique: boolean; + primary: boolean; + valid: boolean; + ready: boolean; + partial: boolean; + predicate: string | null; + columns: { + name: string; + expression: string | null; + order: 'ASC' | 'DESC' | null; + nulls: string | null; + }[]; + }[]; +} + +export type IntrospectedEnum = { + schema_name: string; + enum_type: string; + values: string[]; +}; + +export type IntrospectedSchema = { + tables: IntrospectedTable[]; + enums: IntrospectedEnum[]; +}; + +export type DatabaseFeature = 'Schema' | 'NativeEnum'; + +export interface IntrospectionProvider { + introspect(connectionString: string, options: { schemas: string[]; modelCasing: 'pascal' | 'camel' | 'snake' | 'none' }): Promise; + getBuiltinType(type: string): { + type: BuiltinType | 'Unsupported'; + isArray: boolean; + }; + getDefaultDatabaseType(type: BuiltinType): { precision?: number; type: string } | undefined; + /** + * Get the expression builder callback for a field's @default attribute value. + * Returns null if no @default attribute should be added. + * The callback will be passed to DataFieldAttributeFactory.addArg(). + */ + getDefaultValue(args: { + fieldType: BuiltinType | 'Unsupported'; + datatype: string; + datatype_name: string | null; + defaultValue: string; + services: ZModelServices; + enums: Enum[]; + }): ((builder: ExpressionBuilder) => AstFactory) | null; + /** + * Get additional field attributes based on field type and name (e.g., @updatedAt for DateTime fields, @db.* attributes). + * This is separate from getDefaultValue to keep concerns separated. + */ + getFieldAttributes(args: { + fieldName: string; + fieldType: BuiltinType | 'Unsupported'; + datatype: string; + length: number | null; + precision: number | null; + services: ZModelServices; + }): DataFieldAttributeFactory[]; + isSupportedFeature(feature: DatabaseFeature): boolean; +} diff --git a/packages/cli/src/actions/pull/provider/sqlite.ts b/packages/cli/src/actions/pull/provider/sqlite.ts new file mode 100644 index 000000000..f58ad0b58 --- /dev/null +++ b/packages/cli/src/actions/pull/provider/sqlite.ts @@ -0,0 +1,477 @@ +import { DataFieldAttributeFactory } from '@zenstackhq/language/factory'; +import { getAttributeRef, getDbName, getFunctionRef, normalizeDecimalDefault, normalizeFloatDefault } from '../utils'; +import type { IntrospectedEnum, IntrospectedSchema, IntrospectedTable, IntrospectionProvider } from './provider'; + +// Note: We dynamically import better-sqlite3 inside the async function to avoid +// requiring it at module load time for environments that don't use SQLite. + +export const sqlite: IntrospectionProvider = { + isSupportedFeature(feature) { + switch (feature) { + case 'Schema': + // Multi-schema feature is not available for SQLite because it doesn't have + // the same concept of schemas as namespaces (unlike PostgreSQL, CockroachDB, SQL Server). + return false; + case 'NativeEnum': + // SQLite doesn't support native enum types + return false; + default: + return false; + } + }, + getBuiltinType(type) { + // Strip parenthesized constraints (e.g., VARCHAR(255) → varchar, DECIMAL(10,2) → decimal) + const t = (type || '').toLowerCase().trim().replace(/\(.*\)$/, '').trim(); + // SQLite has no array types + const isArray = false; + + // SQLite type affinity rules (https://www.sqlite.org/datatype3.html): + // 1. If type contains "INT" → INTEGER affinity + // 2. If type contains "CHAR", "CLOB", or "TEXT" → TEXT affinity + // 3. If type contains "BLOB" or no type → BLOB affinity + // 4. If type contains "REAL", "FLOA", or "DOUB" → REAL affinity + // 5. Otherwise → NUMERIC affinity + + // Handle specific known types first for better mapping + switch (t) { + // INTEGER types (SQLite: INT, INTEGER, TINYINT, SMALLINT, MEDIUMINT, INT2, INT8) + case 'integer': + case 'int': + case 'tinyint': + case 'smallint': + case 'mediumint': + case 'int2': + case 'int8': + return { type: 'Int', isArray }; + + // BIGINT - map to BigInt for large integers + case 'bigint': + case 'unsigned big int': + return { type: 'BigInt', isArray }; + + // TEXT types (SQLite: CHARACTER, VARCHAR, VARYING CHARACTER, NCHAR, NATIVE CHARACTER, NVARCHAR, TEXT, CLOB) + case 'text': + case 'varchar': + case 'char': + case 'character': + case 'varying character': + case 'nchar': + case 'native character': + case 'nvarchar': + case 'clob': + return { type: 'String', isArray }; + + // BLOB type + case 'blob': + return { type: 'Bytes', isArray }; + + // REAL types (SQLite: REAL, DOUBLE, DOUBLE PRECISION, FLOAT) + case 'real': + case 'float': + case 'double': + case 'double precision': + return { type: 'Float', isArray }; + + // NUMERIC types (SQLite: NUMERIC, DECIMAL) + case 'numeric': + case 'decimal': + return { type: 'Decimal', isArray }; + + // DateTime types + case 'datetime': + case 'date': + case 'time': + case 'timestamp': + return { type: 'DateTime', isArray }; + + // JSON types + case 'json': + case 'jsonb': + return { type: 'Json', isArray }; + + // Boolean types + case 'boolean': + case 'bool': + return { type: 'Boolean', isArray }; + + default: { + // SQLite affinity rule #3: columns with no declared type have BLOB affinity + if (!t) { + return { type: 'Bytes', isArray }; + } + // Fallback: Use SQLite affinity rules for unknown types + if (t.includes('int')) { + return { type: 'Int', isArray }; + } + if (t.includes('char') || t.includes('clob') || t.includes('text')) { + return { type: 'String', isArray }; + } + if (t.includes('blob')) { + return { type: 'Bytes', isArray }; + } + if (t.includes('real') || t.includes('floa') || t.includes('doub')) { + return { type: 'Float', isArray }; + } + // Default to Unsupported for truly unknown types + return { type: 'Unsupported' as const, isArray }; + } + } + }, + + getDefaultDatabaseType() { + return undefined; + }, + + async introspect(connectionString: string, _options: { schemas: string[]; modelCasing: 'pascal' | 'camel' | 'snake' | 'none' }): Promise { + const SQLite = (await import('better-sqlite3')).default; + const db = new SQLite(connectionString, { readonly: true }); + + try { + const all = (sql: string): T[] => { + const stmt: any = db.prepare(sql); + return stmt.all() as T[]; + }; + + // List user tables and views from sqlite_schema (the master catalog). + // sqlite_schema contains one row per table, view, index, and trigger. + // We filter to only tables/views and exclude internal sqlite_* objects. + // The 'sql' column contains the original CREATE TABLE/VIEW statement. + const tablesRaw = all<{ name: string; type: 'table' | 'view'; definition: string | null }>( + "SELECT name, type, sql AS definition FROM sqlite_schema WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%' ORDER BY name", + ); + + // Detect AUTOINCREMENT by parsing the CREATE TABLE statement + // The sqlite_sequence table only has entries after rows are inserted, + // so we need to check the actual table definition instead + const autoIncrementTables = new Set(); + for (const t of tablesRaw) { + if (t.type === 'table' && t.definition) { + // AUTOINCREMENT keyword appears in PRIMARY KEY definition + // e.g., PRIMARY KEY("id" AUTOINCREMENT) or PRIMARY KEY(id AUTOINCREMENT) + if (/\bAUTOINCREMENT\b/i.test(t.definition)) { + autoIncrementTables.add(t.name); + } + } + } + + const tables: IntrospectedTable[] = []; + + for (const t of tablesRaw) { + const tableName = t.name; + const schema = ''; + + // Check if this table has autoincrement (detected by parsing the CREATE TABLE DDL) + const hasAutoIncrement = autoIncrementTables.has(tableName); + + // PRAGMA table_xinfo: extended version of table_info that also includes hidden/generated columns. + // Returns one row per column with: cid (column index), name, type, notnull, dflt_value, pk. + // hidden: 0 = normal, 1 = hidden (virtual table), 2 = generated VIRTUAL, 3 = generated STORED. + const columnsInfo = all<{ + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; + hidden?: number; + }>(`PRAGMA table_xinfo('${tableName.replace(/'/g, "''")}')`); + + // PRAGMA index_list: returns all indexes on a table. + // Each row has: seq (index sequence), name, unique (1 if unique), origin ('c'=CREATE INDEX, + // 'u'=UNIQUE constraint, 'pk'=PRIMARY KEY), partial (1 if partial index). + // We exclude sqlite_autoindex_* entries which are auto-generated for UNIQUE constraints. + const tableNameEsc = tableName.replace(/'/g, "''"); + const idxList = all<{ + seq: number; + name: string; + unique: number; + origin: string; + partial: number; + }>(`PRAGMA index_list('${tableNameEsc}')`).filter((r) => !r.name.startsWith('sqlite_autoindex_')); + + // Detect single-column unique constraints by inspecting each unique index. + // PRAGMA index_info: returns the columns that make up an index. + // If a unique (non-partial) index has exactly one column, that column is "unique". + const uniqueSingleColumn = new Set(); + const uniqueIndexRows = idxList.filter((r) => r.unique === 1 && r.partial !== 1); + for (const idx of uniqueIndexRows) { + const idxCols = all<{ name: string }>(`PRAGMA index_info('${idx.name.replace(/'/g, "''")}')`); + if (idxCols.length === 1 && idxCols[0]?.name) { + uniqueSingleColumn.add(idxCols[0].name); + } + } + + // Build detailed index info for each index. + // PRAGMA index_info returns one row per column in the index. + // SQLite doesn't expose access method, predicate, or sort order through PRAGMAs. + const indexes: IntrospectedTable['indexes'] = idxList.map((idx) => { + const idxCols = all<{ name: string }>(`PRAGMA index_info('${idx.name.replace(/'/g, "''")}')`); + return { + name: idx.name, + method: null, // SQLite does not expose index method + unique: idx.unique === 1, + primary: false, // SQLite does not expose this directly; handled via pk in columns + valid: true, // SQLite does not expose index validity + ready: true, // SQLite does not expose index readiness + partial: idx.partial === 1, + predicate: idx.partial === 1 ? '[partial]' : null, // SQLite does not expose index predicate + columns: idxCols.map((col) => ({ + name: col.name, + expression: null, + order: null, + nulls: null, + })), + }; + }); + + // PRAGMA foreign_key_list: returns all foreign key constraints on a table. + // Each row represents one column in a FK constraint with: id (FK id, shared by multi-column FKs), + // seq (column index within the FK), table (referenced table), from (local column), + // to (referenced column), on_update, on_delete (referential actions). + const fkRows = all<{ + id: number; + seq: number; + table: string; + from: string; + to: string | null; + on_update: any; + on_delete: any; + }>(`PRAGMA foreign_key_list('${tableName.replace(/'/g, "''")}')`); + + // Extract FK constraint names from CREATE TABLE statement. + // Captures the constraint name and the full parenthesized column list from + // FOREIGN KEY(...), then splits and parses individual column names so that + // composite FKs (e.g., FOREIGN KEY("col1", "col2")) are handled correctly. + const fkConstraintNames = new Map(); + if (t.definition) { + // Match: CONSTRAINT "name" FOREIGN KEY() + // Group 1/2: quoted/unquoted constraint name + // Group 3: the full content inside FOREIGN KEY(...) + const fkRegex = /CONSTRAINT\s+(?:["'`]([^"'`]+)["'`]|(\w+))\s+FOREIGN\s+KEY\s*\(([^)]+)\)/gi; + let match; + while ((match = fkRegex.exec(t.definition)) !== null) { + const constraintName = match[1] || match[2]; + const columnList = match[3]; + if (constraintName && columnList) { + // Split the column list on commas and strip quotes/whitespace + // to extract each individual column name. + const columns = columnList.split(',').map((col) => col.trim().replace(/^["'`]|["'`]$/g, '')); + for (const col of columns) { + if (col) { + fkConstraintNames.set(col, constraintName); + } + } + } + } + } + + const fkByColumn = new Map< + string, + { + foreign_key_schema: string | null; + foreign_key_table: string | null; + foreign_key_column: string | null; + foreign_key_name: string | null; + foreign_key_on_update: IntrospectedTable['columns'][number]['foreign_key_on_update']; + foreign_key_on_delete: IntrospectedTable['columns'][number]['foreign_key_on_delete']; + } + >(); + + for (const fk of fkRows) { + fkByColumn.set(fk.from, { + foreign_key_schema: '', + foreign_key_table: fk.table || null, + foreign_key_column: fk.to || null, + foreign_key_name: fkConstraintNames.get(fk.from) ?? null, + foreign_key_on_update: (fk.on_update as any) ?? null, + foreign_key_on_delete: (fk.on_delete as any) ?? null, + }); + } + + // Pre-extract full column type definitions from DDL for generated columns. + // PRAGMA table_xinfo only returns the base type (e.g., "TEXT"), but for + // generated columns we need the full definition including the expression + // (e.g., "TEXT GENERATED ALWAYS AS (...) STORED") so they are pulled as + // Unsupported("...") — matching Prisma's introspection behavior. + const generatedColDefs = t.definition ? extractColumnTypeDefs(t.definition) : new Map(); + + const columns: IntrospectedTable['columns'] = []; + for (const c of columnsInfo) { + // hidden: 0 = normal, 1 = hidden (virtual table) → skip, + // 2 = generated VIRTUAL, 3 = generated STORED → mark computed + const hidden = c.hidden ?? 0; + if (hidden === 1) continue; + + const isGenerated = hidden === 2 || hidden === 3; + + const fk = fkByColumn.get(c.name); + + // Determine default value - check for autoincrement + // AUTOINCREMENT in SQLite can only be on INTEGER PRIMARY KEY column + let defaultValue = c.dflt_value; + if (hasAutoIncrement && c.pk) { + defaultValue = 'autoincrement'; + } + + // For generated columns, use the full DDL type definition so that + // getBuiltinType returns Unsupported and the column is rendered as + // Unsupported("TYPE GENERATED ALWAYS AS (...) STORED/VIRTUAL"). + let datatype = c.type || ''; + if (isGenerated) { + const fullDef = generatedColDefs.get(c.name); + if (fullDef) { + datatype = fullDef; + } + } + + columns.push({ + name: c.name, + datatype, + datatype_name: null, // SQLite doesn't support native enums + length: null, + precision: null, + datatype_schema: schema, + foreign_key_schema: fk?.foreign_key_schema ?? null, + foreign_key_table: fk?.foreign_key_table ?? null, + foreign_key_column: fk?.foreign_key_column ?? null, + foreign_key_name: fk?.foreign_key_name ?? null, + foreign_key_on_update: fk?.foreign_key_on_update ?? null, + foreign_key_on_delete: fk?.foreign_key_on_delete ?? null, + pk: !!c.pk, + computed: isGenerated, + nullable: c.notnull !== 1, + default: defaultValue, + unique: uniqueSingleColumn.has(c.name), + unique_name: null, + }); + } + + tables.push({ schema, name: tableName, columns, type: t.type, definition: t.definition, indexes }); + } + + const enums: IntrospectedEnum[] = []; // SQLite doesn't support enums + + return { tables, enums }; + } finally { + db.close(); + } + }, + + getDefaultValue({ defaultValue, fieldType, services, enums }) { // datatype and datatype_name not used for SQLite + const val = defaultValue.trim(); + + switch (fieldType) { + case 'DateTime': + if (val === 'CURRENT_TIMESTAMP' || val === 'now()') { + return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('now', services)); + } + // Fallback to string literal for other DateTime defaults + return (ab) => ab.StringLiteral.setValue(val); + + case 'Int': + case 'BigInt': + if (val === 'autoincrement') { + return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('autoincrement', services)); + } + return (ab) => ab.NumberLiteral.setValue(val); + + case 'Float': + return normalizeFloatDefault(val); + + case 'Decimal': + return normalizeDecimalDefault(val); + + case 'Boolean': + return (ab) => ab.BooleanLiteral.setValue(val === 'true' || val === '1'); + case 'String': + if (val.startsWith("'") && val.endsWith("'")) { + const strippedName = val.slice(1, -1); + const enumDef = enums.find((e) => e.fields.find((v) => getDbName(v) === strippedName)); + if (enumDef) { + const enumField = enumDef.fields.find((v) => getDbName(v) === strippedName); + if (enumField) return (ab) => ab.ReferenceExpr.setTarget(enumField); + } + return (ab) => ab.StringLiteral.setValue(strippedName); + } + return (ab) => ab.StringLiteral.setValue(val); + } + + console.warn(`Unsupported default value type: "${defaultValue}" for field type "${fieldType}". Skipping default value.`); + return null; + }, + + getFieldAttributes({ fieldName, fieldType, services }) { + const factories: DataFieldAttributeFactory[] = []; + + // Add @updatedAt for DateTime fields named updatedAt or updated_at + if (fieldType === 'DateTime' && (fieldName.toLowerCase() === 'updatedat' || fieldName.toLowerCase() === 'updated_at')) { + factories.push(new DataFieldAttributeFactory().setDecl(getAttributeRef('@updatedAt', services))); + } + + return factories; + }, +}; + +/** + * Extract column type definitions from a CREATE TABLE DDL statement. + * Returns a map of column name → full type definition string (everything after the column name). + * Used to get the complete type including GENERATED ALWAYS AS (...) STORED/VIRTUAL for generated columns. + */ +function extractColumnTypeDefs(ddl: string): Map { + // Find the content inside CREATE TABLE "name" ( ... ) + // Use a paren-depth approach to find the matching closing paren. + const openIdx = ddl.indexOf('('); + if (openIdx === -1) return new Map(); + + let depth = 1; + let closeIdx = -1; + for (let i = openIdx + 1; i < ddl.length; i++) { + if (ddl[i] === '(') depth++; + else if (ddl[i] === ')') { + depth--; + if (depth === 0) { + closeIdx = i; + break; + } + } + } + if (closeIdx === -1) return new Map(); + + const content = ddl.substring(openIdx + 1, closeIdx); + + // Split column definitions on commas, respecting nested parentheses. + const defs: string[] = []; + let current = ''; + depth = 0; + for (const char of content) { + if (char === '(') depth++; + else if (char === ')') depth--; + else if (char === ',' && depth === 0) { + defs.push(current.trim()); + current = ''; + continue; + } + current += char; + } + if (current.trim()) defs.push(current.trim()); + + // Map column name → type definition (everything after the column name). + // Table constraints (CONSTRAINT, PRIMARY KEY, UNIQUE, FOREIGN KEY, CHECK) + // are skipped since they don't define columns. + const result = new Map(); + for (const def of defs) { + // Match: optional quote + column name + optional quote + whitespace + type definition + const nameMatch = def.match(/^(?:["'`]([^"'`]+)["'`]|(\w+))\s+(.+)/s); + if (nameMatch) { + const name = nameMatch[1] || nameMatch[2]; + const typeDef = nameMatch[3]; + // Skip table-level constraints (they start with keywords, not column names, + // but could still match the regex — the map lookup by actual column name + // ensures they never interfere). + if (name && typeDef) { + result.set(name, typeDef.trim()); + } + } + } + return result; +} diff --git a/packages/cli/src/actions/pull/utils.ts b/packages/cli/src/actions/pull/utils.ts new file mode 100644 index 000000000..e0abcfdfd --- /dev/null +++ b/packages/cli/src/actions/pull/utils.ts @@ -0,0 +1,214 @@ +import type { ZModelServices } from '@zenstackhq/language'; +import { + type AbstractDeclaration, + type DataField, + type DataModel, + type Enum, + type EnumField, + type Expression, + type FunctionDecl, + isInvocationExpr, + type Attribute, + type Model, + type ReferenceExpr, + type StringLiteral, +} from '@zenstackhq/language/ast'; +import type { AstFactory, ExpressionBuilder } from '@zenstackhq/language/factory'; +import { getLiteralArray, getStringLiteral } from '@zenstackhq/language/utils'; +import type { DataSourceProviderType } from '@zenstackhq/schema'; +import type { Reference } from 'langium'; +import { CliError } from '../../cli-error'; + +export function getAttribute(model: Model, attrName: string) { + if (!model.$document) throw new CliError('Model is not associated with a document.'); + + const references = model.$document.references as Reference[]; + return references.find((a) => a.ref?.$type === 'Attribute' && a.ref?.name === attrName)?.ref as + | Attribute + | undefined; +} + +export function getDatasource(model: Model) { + const datasource = model.declarations.find((d) => d.$type === 'DataSource'); + if (!datasource) { + throw new CliError('No datasource declaration found in the schema.'); + } + + const urlField = datasource.fields.find((f) => f.name === 'url'); + + if (!urlField) throw new CliError(`No url field found in the datasource declaration.`); + + let url = getStringLiteral(urlField.value); + + if (!url && isInvocationExpr(urlField.value)) { + const envName = getStringLiteral(urlField.value.args[0]?.value); + if (!envName) { + throw new CliError('The url field must be a string literal or an env().'); + } + if (!process.env[envName]) { + throw new CliError( + `Environment variable ${envName} is not set, please set it to the database connection string.`, + ); + } + url = process.env[envName]; + } + + if (!url) { + throw new CliError('The url field must be a string literal or an env().'); + } + + if (url.startsWith('file:')) { + url = new URL(url, `file:${model.$document!.uri.path}`).pathname; + if (process.platform === 'win32' && url[0] === '/') url = url.slice(1); + } + + const defaultSchemaField = datasource.fields.find((f) => f.name === 'defaultSchema'); + const defaultSchema = (defaultSchemaField && getStringLiteral(defaultSchemaField.value)) || 'public'; + + const schemasField = datasource.fields.find((f) => f.name === 'schemas'); + const schemas = + (schemasField && + getLiteralArray(schemasField.value) + ?.filter((s) => s !== undefined)) as string[] || + []; + + const provider = getStringLiteral( + datasource.fields.find((f) => f.name === 'provider')?.value, + ); + if (!provider) { + throw new CliError(`Datasource "${datasource.name}" is missing a "provider" field.`); + } + + return { + name: datasource.name, + provider: provider as DataSourceProviderType, + url, + defaultSchema, + schemas, + allSchemas: [defaultSchema, ...schemas], + }; +} + +export function getDbName(decl: AbstractDeclaration | DataField | EnumField, includeSchema: boolean = false): string { + if (!('attributes' in decl)) return decl.name; + + const schemaAttr = decl.attributes.find((a) => a.decl.ref?.name === '@@schema'); + let schema = 'public'; + if (schemaAttr) { + const schemaAttrValue = schemaAttr.args[0]?.value; + if (schemaAttrValue?.$type === 'StringLiteral') { + schema = schemaAttrValue.value; + } + } + + const formatName = (name: string) => `${schema && includeSchema ? `${schema}.` : ''}${name}`; + + const nameAttr = decl.attributes.find((a) => a.decl.ref?.name === '@@map' || a.decl.ref?.name === '@map'); + if (!nameAttr) return formatName(decl.name); + const attrValue = nameAttr.args[0]?.value; + + if (attrValue?.$type !== 'StringLiteral') return formatName(decl.name); + + return formatName(attrValue.value); +} + +export function getRelationFkName(decl: DataField): string | undefined { + const relationAttr = decl?.attributes.find((a) => a.decl.ref?.name === '@relation'); + const schemaAttrValue = relationAttr?.args.find((a) => a.name === 'map')?.value as StringLiteral; + return schemaAttrValue?.value; +} + +/** + * Gets the FK field names from the @relation attribute's `fields` argument. + * Returns a sorted, comma-separated string of field names for comparison. + * e.g., @relation(fields: [userId], references: [id]) -> "userId" + * e.g., @relation(fields: [postId, tagId], references: [id, id]) -> "postId,tagId" + */ +export function getRelationFieldsKey(decl: DataField): string | undefined { + const relationAttr = decl?.attributes.find((a) => a.decl.ref?.name === '@relation'); + if (!relationAttr) return undefined; + + const fieldsArg = relationAttr.args.find((a) => a.name === 'fields')?.value; + if (!fieldsArg || fieldsArg.$type !== 'ArrayExpr') return undefined; + + const fieldNames = fieldsArg.items + .filter((item): item is ReferenceExpr => item.$type === 'ReferenceExpr') + .map((item) => item.target?.$refText || item.target?.ref?.name) + .filter((name): name is string => !!name) + .sort(); + + return fieldNames.length > 0 ? fieldNames.join(',') : undefined; +} + +export function getDbSchemaName(decl: DataModel | Enum): string { + const schemaAttr = decl.attributes.find((a) => a.decl.ref?.name === '@@schema'); + if (!schemaAttr) return 'public'; + const attrValue = schemaAttr.args[0]?.value; + + if (attrValue?.$type !== 'StringLiteral') return 'public'; + + return attrValue.value; +} + +export function getDeclarationRef( + type: T['$type'], + name: string, + services: ZModelServices, +) { + const node = services.shared.workspace.IndexManager.allElements(type).find( + (m) => m.node && getDbName(m.node as T) === name, + )?.node; + if (!node) throw new CliError(`Declaration not found: ${name}`); + return node as T; +} + +export function getEnumRef(name: string, services: ZModelServices) { + return getDeclarationRef('Enum', name, services); +} + +export function getModelRef(name: string, services: ZModelServices) { + return getDeclarationRef('DataModel', name, services); +} + +export function getAttributeRef(name: string, services: ZModelServices) { + return getDeclarationRef('Attribute', name, services); +} + +export function getFunctionRef(name: string, services: ZModelServices) { + return getDeclarationRef('FunctionDecl', name, services); +} + +/** + * Normalize a default value string for a Float field. + * - Integer strings get `.0` appended + * - Decimal strings are preserved as-is + */ +export function normalizeFloatDefault(val: string): (ab: ExpressionBuilder) => AstFactory { + if (/^-?\d+$/.test(val)) { + return (ab) => ab.NumberLiteral.setValue(val + '.0'); + } + if (/^-?\d+\.\d+$/.test(val)) { + return (ab) => ab.NumberLiteral.setValue(val); + } + return (ab) => ab.NumberLiteral.setValue(val); +} + +/** + * Normalize a default value string for a Decimal field. + * - Integer strings get `.00` appended + * - Decimal strings are normalized to minimum 2 decimal places, stripping excess trailing zeros + */ +export function normalizeDecimalDefault(val: string): (ab: ExpressionBuilder) => AstFactory { + if (/^-?\d+$/.test(val)) { + return (ab) => ab.NumberLiteral.setValue(val + '.00'); + } + if (/^-?\d+\.\d+$/.test(val)) { + const [integerPart, fractionalPart] = val.split('.'); + let normalized = fractionalPart!.replace(/0+$/, ''); + if (normalized.length < 2) { + normalized = normalized.padEnd(2, '0'); + } + return (ab) => ab.NumberLiteral.setValue(`${integerPart}.${normalized}`); + } + return (ab) => ab.NumberLiteral.setValue(val); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4efc86fd9..6e1daafec 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -143,6 +143,36 @@ function createProgram() { .addOption(new Option('--force-reset', 'force a reset of the database before push')) .action((options) => dbAction('push', options)); + dbCommand + .command('pull') + .description('Introspect your database.') + .addOption(schemaOption) + .addOption(noVersionCheckOption) + .addOption( + new Option( + '-o, --output ', + 'set custom output path for the introspected schema. If a file path is provided, all schemas are merged into that single file. If a directory path is provided, files are written to the directory and imports are kept.', + ), + ) + .addOption( + new Option('--model-casing ', 'set the casing of generated models').default( + 'pascal', + ), + ) + .addOption( + new Option('--field-casing ', 'set the casing of generated fields').default( + 'camel', + ), + ) + .addOption( + new Option('--always-map', 'always add @map and @@map attributes to models and fields').default(false), + ) + .addOption( + new Option('--quote ', 'set the quote style of generated schema files').default('single'), + ) + .addOption(new Option('--indent ', 'set the indentation of the generated schema files').default(4).argParser(parseInt)) + .action((options) => dbAction('pull', options)); + dbCommand .command('seed') .description('Seed the database') diff --git a/packages/cli/test/casing.test.ts b/packages/cli/test/casing.test.ts new file mode 100644 index 000000000..60b9d1c88 --- /dev/null +++ b/packages/cli/test/casing.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from 'vitest'; +import { resolveNameCasing, toPascalCase, toCamelCase, toSnakeCase } from '../src/actions/pull/casing'; + +describe('toPascalCase', () => { + it('converts snake_case', () => { + expect(toPascalCase('user_status')).toBe('UserStatus'); + expect(toPascalCase('first_name')).toBe('FirstName'); + }); + + it('converts kebab-case', () => { + expect(toPascalCase('user-status')).toBe('UserStatus'); + }); + + it('capitalizes first char of lowercase', () => { + expect(toPascalCase('user')).toBe('User'); + }); + + it('preserves already PascalCase', () => { + expect(toPascalCase('UserStatus')).toBe('UserStatus'); + }); + + it('preserves all-uppercase strings', () => { + expect(toPascalCase('ACTIVE')).toBe('ACTIVE'); + expect(toPascalCase('USER')).toBe('USER'); + expect(toPascalCase('MODERATOR')).toBe('MODERATOR'); + expect(toPascalCase('SET_NULL')).toBe('SET_NULL'); + expect(toPascalCase('NO_ACTION')).toBe('NO_ACTION'); + }); + + it('converts mixed snake_case with uppercase', () => { + expect(toPascalCase('User_status')).toBe('UserStatus'); + }); +}); + +describe('toCamelCase', () => { + it('converts snake_case', () => { + expect(toCamelCase('user_status')).toBe('userStatus'); + expect(toCamelCase('first_name')).toBe('firstName'); + }); + + it('converts kebab-case', () => { + expect(toCamelCase('user-status')).toBe('userStatus'); + }); + + it('lowercases first char of PascalCase', () => { + expect(toCamelCase('User')).toBe('user'); + expect(toCamelCase('Post')).toBe('post'); + }); + + it('preserves already camelCase', () => { + expect(toCamelCase('userStatus')).toBe('userStatus'); + }); + + it('preserves all-uppercase strings', () => { + expect(toCamelCase('ACTIVE')).toBe('ACTIVE'); + expect(toCamelCase('INACTIVE')).toBe('INACTIVE'); + expect(toCamelCase('SUSPENDED')).toBe('SUSPENDED'); + expect(toCamelCase('USER')).toBe('USER'); + expect(toCamelCase('SET_NULL')).toBe('SET_NULL'); + expect(toCamelCase('NO_ACTION')).toBe('NO_ACTION'); + }); +}); + +describe('toSnakeCase', () => { + it('converts camelCase', () => { + expect(toSnakeCase('userStatus')).toBe('user_status'); + expect(toSnakeCase('firstName')).toBe('first_name'); + }); + + it('converts PascalCase', () => { + expect(toSnakeCase('UserStatus')).toBe('user_status'); + }); + + it('converts kebab-case', () => { + expect(toSnakeCase('user-status')).toBe('user_status'); + }); + + it('preserves already snake_case', () => { + expect(toSnakeCase('user_status')).toBe('user_status'); + }); + + it('preserves all-uppercase strings', () => { + expect(toSnakeCase('ACTIVE')).toBe('ACTIVE'); + expect(toSnakeCase('INACTIVE')).toBe('INACTIVE'); + expect(toSnakeCase('SUSPENDED')).toBe('SUSPENDED'); + expect(toSnakeCase('SET_NULL')).toBe('SET_NULL'); + expect(toSnakeCase('NO_ACTION')).toBe('NO_ACTION'); + }); +}); + +describe('resolveNameCasing', () => { + it('applies pascal casing', () => { + expect(resolveNameCasing('pascal', 'user_status')).toEqual({ modified: true, name: 'UserStatus' }); + expect(resolveNameCasing('pascal', 'User')).toEqual({ modified: false, name: 'User' }); + }); + + it('applies camel casing', () => { + expect(resolveNameCasing('camel', 'User')).toEqual({ modified: true, name: 'user' }); + expect(resolveNameCasing('camel', 'first_name')).toEqual({ modified: true, name: 'firstName' }); + }); + + it('applies snake casing', () => { + expect(resolveNameCasing('snake', 'UserStatus')).toEqual({ modified: true, name: 'user_status' }); + expect(resolveNameCasing('snake', 'user_status')).toEqual({ modified: false, name: 'user_status' }); + }); + + it('preserves name with none casing', () => { + expect(resolveNameCasing('none', 'User_status')).toEqual({ modified: false, name: 'User_status' }); + expect(resolveNameCasing('none', 'ACTIVE')).toEqual({ modified: false, name: 'ACTIVE' }); + }); + + it('preserves all-uppercase enum values across all casings', () => { + expect(resolveNameCasing('pascal', 'ACTIVE')).toEqual({ modified: false, name: 'ACTIVE' }); + expect(resolveNameCasing('camel', 'ACTIVE')).toEqual({ modified: false, name: 'ACTIVE' }); + expect(resolveNameCasing('snake', 'ACTIVE')).toEqual({ modified: false, name: 'ACTIVE' }); + expect(resolveNameCasing('none', 'ACTIVE')).toEqual({ modified: false, name: 'ACTIVE' }); + }); + + it('preserves all-uppercase enum values with underscores across all casings', () => { + expect(resolveNameCasing('pascal', 'SET_NULL')).toEqual({ modified: false, name: 'SET_NULL' }); + expect(resolveNameCasing('camel', 'SET_NULL')).toEqual({ modified: false, name: 'SET_NULL' }); + expect(resolveNameCasing('snake', 'SET_NULL')).toEqual({ modified: false, name: 'SET_NULL' }); + expect(resolveNameCasing('none', 'SET_NULL')).toEqual({ modified: false, name: 'SET_NULL' }); + }); + + it('prefixes names starting with a digit', () => { + expect(resolveNameCasing('none', '1foo')).toEqual({ modified: true, name: '_1foo' }); + expect(resolveNameCasing('camel', '1foo')).toEqual({ modified: true, name: '_1foo' }); + }); +}); diff --git a/packages/cli/test/check.test.ts b/packages/cli/test/check.test.ts index 287bb6b80..99d31ecda 100644 --- a/packages/cli/test/check.test.ts +++ b/packages/cli/test/check.test.ts @@ -36,37 +36,37 @@ model Post { `; describe('CLI validate command test', () => { - it('should validate a valid schema successfully', () => { - const workDir = createProject(validModel); + it('should validate a valid schema successfully', async () => { + const { workDir } = await createProject(validModel); // Should not throw an error expect(() => runCli('check', workDir)).not.toThrow(); }); - it('should fail validation for invalid schema', () => { - const workDir = createProject(invalidModel); + it('should fail validation for invalid schema', async () => { + const { workDir } = await createProject(invalidModel); // Should throw an error due to validation failure expect(() => runCli('check', workDir)).toThrow(); }); - it('should respect custom schema location', () => { - const workDir = createProject(validModel); + it('should respect custom schema location', async () => { + const { workDir } = await createProject(validModel); fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/custom.zmodel')); // Should not throw an error when using custom schema path expect(() => runCli('check --schema ./zenstack/custom.zmodel', workDir)).not.toThrow(); }); - it('should fail when schema file does not exist', () => { - const workDir = createProject(validModel); + it('should fail when schema file does not exist', async () => { + const { workDir } = await createProject(validModel); // Should throw an error when schema file doesn't exist expect(() => runCli('check --schema ./nonexistent.zmodel', workDir)).toThrow(); }); - it('should respect package.json config', () => { - const workDir = createProject(validModel); + it('should respect package.json config', async () => { + const { workDir } = await createProject(validModel); fs.mkdirSync(path.join(workDir, 'foo')); fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel')); fs.rmdirSync(path.join(workDir, 'zenstack')); @@ -81,19 +81,14 @@ describe('CLI validate command test', () => { expect(() => runCli('check', workDir)).not.toThrow(); }); - it('should validate schema with syntax errors', () => { + it('should validate schema with syntax errors', async () => { const modelWithSyntaxError = ` -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - model User { id String @id @default(cuid()) email String @unique // Missing closing brace - syntax error `; - const workDir = createProject(modelWithSyntaxError, false); + const { workDir } = await createProject(modelWithSyntaxError); // Should throw an error due to syntax error expect(() => runCli('check', workDir)).toThrow(); diff --git a/packages/cli/test/db.test.ts b/packages/cli/test/db.test.ts index 636dcff8f..b5b76d4fa 100644 --- a/packages/cli/test/db.test.ts +++ b/packages/cli/test/db.test.ts @@ -10,14 +10,14 @@ model User { `; describe('CLI db commands test', () => { - it('should generate a database with db push', () => { - const workDir = createProject(model); + it('should generate a database with db push', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); runCli('db push', workDir); - expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); + expect(fs.existsSync(path.join(workDir, 'zenstack/test.db'))).toBe(true); }); - it('should seed the database with db seed with seed script', () => { - const workDir = createProject(model); + it('should seed the database with db seed with seed script', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8')); pkgJson.zenstack = { seed: 'node seed.js', @@ -35,8 +35,8 @@ fs.writeFileSync('seed.txt', 'success'); expect(fs.readFileSync(path.join(workDir, 'seed.txt'), 'utf8')).toBe('success'); }); - it('should seed the database after migrate reset', () => { - const workDir = createProject(model); + it('should seed the database after migrate reset', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); const pkgJson = JSON.parse(fs.readFileSync(path.join(workDir, 'package.json'), 'utf8')); pkgJson.zenstack = { seed: 'node seed.js', @@ -54,8 +54,8 @@ fs.writeFileSync('seed.txt', 'success'); expect(fs.readFileSync(path.join(workDir, 'seed.txt'), 'utf8')).toBe('success'); }); - it('should skip seeding the database without seed script', () => { - const workDir = createProject(model); + it('should skip seeding the database without seed script', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); runCli('db seed', workDir); }); }); diff --git a/packages/cli/test/db/pull.test.ts b/packages/cli/test/db/pull.test.ts new file mode 100644 index 000000000..487f6a446 --- /dev/null +++ b/packages/cli/test/db/pull.test.ts @@ -0,0 +1,1123 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createProject, getDefaultPrelude, getTestDbName, getTestDbUrl, runCli } from '../utils'; +import { formatDocument } from '@zenstackhq/language'; +import { getTestDbProvider } from '@zenstackhq/testtools'; + +const getSchema = (workDir: string) => fs.readFileSync(path.join(workDir, 'zenstack/schema.zmodel')).toString(); + +describe('DB pull - Common features (all providers)', () => { + describe('Pull from zero - restore complete schema from database', () => { + it('should restore basic schema with all supported types', async () => { + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + age Int @default(0) + balance Decimal @default(0.00) + isActive Boolean @default(true) + bigCounter BigInt @default(0) + score Float @default(0.0) + bio String? + avatar Bytes? + metadata Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +}`, + ); + runCli('db push', workDir); + + // Store the schema after db push (this is what provider names will be) + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + // Remove schema content to simulate restoration from zero + fs.writeFileSync(schemaFile, getDefaultPrelude()); + + // Pull should fully restore the schema + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should restore schema with relations', async () => { + const { workDir, schema } = await createProject( + `model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId Int +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +}`, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should restore schema with many-to-many relations', async () => { + const { workDir, schema } = await createProject( + `model Post { + id Int @id @default(autoincrement()) + title String + postTags PostTag[] +} + +model PostTag { + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + postId Int + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + tagId Int + + @@id([postId, tagId]) +} + +model Tag { + id Int @id @default(autoincrement()) + name String @unique + postTags PostTag[] +}`, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should restore one-to-one relation when FK is the single-column primary key', async () => { + const { workDir, schema } = await createProject( + `model Profile { + user User @relation(fields: [id], references: [id], onDelete: Cascade) + id Int @id @default(autoincrement()) + bio String? +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + profile Profile? +}`, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should restore schema with indexes and unique constraints', async () => { + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + username String + firstName String + lastName String + role String + + @@unique([username, email]) + @@index([role]) + @@index([firstName, lastName]) + @@index([email, username, role]) +}`, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should restore schema with composite primary keys', async () => { + const { workDir, schema } = await createProject( + `model UserRole { + userId String + role String + grantedAt DateTime @default(now()) + + @@id([userId, role]) +}`, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should preserve Decimal and Float default value precision', async () => { + const { workDir, schema } = await createProject( + `model Product { + id Int @id @default(autoincrement()) + price Decimal @default(99.99) + discount Decimal @default(0.50) + taxRate Decimal @default(7.00) + weight Float @default(1.5) + rating Float @default(4.0) + temperature Float @default(98.6) +}`, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + }); + + describe('Pull with existing schema - preserve schema features', () => { + it('should preserve field and table mappings', async () => { + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique @map('email_address') + firstName String @map('first_name') + lastName String @map('last_name') + + @@map('users') +}`, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + + expect(getSchema(workDir)).toEqual(schema); + }); + + it('should not modify a comprehensive schema with all features', async () => { + const { workDir, schema } = await createProject(`model User { + id Int @id @default(autoincrement()) + email String @unique @map('email_address') + name String? @default('Anonymous') + role UsersRole @default(USER) + profile Profile? + shared_profile Profile? @relation('shared') + posts Post[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + jsonData Json? + balance Decimal @default(0.00) + isActive Boolean @default(true) + bigCounter BigInt @default(0) + bytes Bytes? + + @@index([role]) + @@map('users') +} + +model Profile { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int @unique + user_shared User @relation('shared', fields: [shared_userId], references: [id], onDelete: Cascade) + shared_userId Int @unique + bio String? + avatarUrl String? + + @@map('profiles') +} + +model Post { + id Int @id @default(autoincrement()) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId Int + title String + content String? + published Boolean @default(false) + tags PostTag[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + slug String + score Float @default(0.0) + metadata Json? + + @@unique([authorId, slug]) + @@index([authorId, published]) + @@map('posts') +} + +model Tag { + id Int @id @default(autoincrement()) + name String @unique + posts PostTag[] + createdAt DateTime @default(now()) + + @@index([name], name: 'tag_name_idx') + @@map('tags') +} + +model PostTag { + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + postId Int + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + tagId Int + assignedAt DateTime @default(now()) + note String? @default('initial') + + @@id([postId, tagId]) + @@map('post_tags') +} + +enum UsersRole { + USER + ADMIN + MODERATOR +}`, +// When using MySQL, the introspection simply overrides the enum and cannot detect if it exists with the same name because it only stores the values. +// TODO: Create a better way to handle this, possibly by finding enums by their values as well if the schema exists. + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + expect(getSchema(workDir)).toEqual(schema); + }); + + it('should preserve imports when pulling with multi-file schema', async () => { + const { workDir } = await createProject('', { customPrelude: true }); + const schemaPath = path.join(workDir, 'zenstack/schema.zmodel'); + const modelsDir = path.join(workDir, 'zenstack/models'); + + fs.mkdirSync(modelsDir, { recursive: true }); + + // Create main schema with imports + const mainSchema = await formatDocument(`import './models/user' +import './models/post' + +${getDefaultPrelude()}`); + fs.writeFileSync(schemaPath, mainSchema); + + // Create user model + const userModel = await formatDocument(`import './post' + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + posts Post[] + createdAt DateTime @default(now()) +}`); + fs.writeFileSync(path.join(modelsDir, 'user.zmodel'), userModel); + + // Create post model + const postModel = await formatDocument(`import './user' + +model Post { + id Int @id @default(autoincrement()) + title String + content String? + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId Int + createdAt DateTime @default(now()) +}`); + fs.writeFileSync(path.join(modelsDir, 'post.zmodel'), postModel); + + runCli('db push', workDir); + + // Pull and verify imports are preserved + runCli('db pull --indent 4', workDir); + + const pulledMainSchema = fs.readFileSync(schemaPath).toString(); + const pulledUserSchema = fs.readFileSync(path.join(modelsDir, 'user.zmodel')).toString(); + const pulledPostSchema = fs.readFileSync(path.join(modelsDir, 'post.zmodel')).toString(); + + expect(pulledMainSchema).toEqual(mainSchema); + expect(pulledUserSchema).toEqual(userModel); + expect(pulledPostSchema).toEqual(postModel); + }); + }); + + describe('Pull should update existing field definitions when database changes', () => { + it('should update field type when database column type changes', async () => { + // Step 1: Create initial schema with String field + const { workDir } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + age String +}`, + ); + runCli('db push', workDir); + + // Step 2: Modify schema to change age from String to Int + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + const updatedSchema = await formatDocument(`${getDefaultPrelude()} + +model User { + id Int @id @default(autoincrement()) + email String @unique + age Int +}`); + fs.writeFileSync(schemaFile, updatedSchema); + runCli('db push', workDir); + + // Step 3: Revert schema back to original (with String type) + const originalSchema = await formatDocument(`${getDefaultPrelude()} + +model User { + id Int @id @default(autoincrement()) + email String @unique + age String +}`); + fs.writeFileSync(schemaFile, originalSchema); + + // Step 4: Pull from database - should detect that age is now Int + runCli('db pull --indent 4', workDir); + + // Step 5: Verify that pulled schema has Int type (matching database) + const pulledSchema = getSchema(workDir); + expect(pulledSchema).toEqual(updatedSchema); + }); + + it('should update field optionality when database column nullability changes', async () => { + // Step 1: Create initial schema with required field + const { workDir } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + name String +}`, + ); + runCli('db push', workDir); + + // Step 2: Modify schema to make name optional + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + const updatedSchema = await formatDocument(`${getDefaultPrelude()} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +}`); + fs.writeFileSync(schemaFile, updatedSchema); + runCli('db push', workDir); + + // Step 3: Revert schema back to original (with required name) + const originalSchema = await formatDocument(`${getDefaultPrelude()} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String +}`); + fs.writeFileSync(schemaFile, originalSchema); + + // Step 4: Pull from database - should detect that name is now optional + runCli('db pull --indent 4', workDir); + + // Step 5: Verify that pulled schema has optional name (matching database) + const pulledSchema = getSchema(workDir); + expect(pulledSchema).toEqual(updatedSchema); + }); + + it('should update default value when database default changes', async () => { + // Step 1: Create initial schema with default value + const { workDir } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + status String @default('active') +}`, + ); + runCli('db push', workDir); + + // Step 2: Modify schema to change default value + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + const updatedSchema = await formatDocument(`${getDefaultPrelude()} + +model User { + id Int @id @default(autoincrement()) + email String @unique + status String @default('pending') +}`); + fs.writeFileSync(schemaFile, updatedSchema); + runCli('db push', workDir); + + // Step 3: Revert schema back to original default + const originalSchema = await formatDocument(`${getDefaultPrelude()} + +model User { + id Int @id @default(autoincrement()) + email String @unique + status String @default('active') +}`); + fs.writeFileSync(schemaFile, originalSchema); + + // Step 4: Pull from database - should detect that default changed + runCli('db pull --indent 4', workDir); + + // Step 5: Verify that pulled schema has updated default (matching database) + const pulledSchema = getSchema(workDir); + expect(pulledSchema).toEqual(updatedSchema); + }); + }); +}); + +describe('DB pull - PostgreSQL specific features', () => { + it('should restore schema with multiple database schemas', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'postgresql') { + skip(); + return; + } + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] + + @@schema('auth') +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId Int + + @@schema('content') +}`, + { provider: 'postgresql', datasourceFields:{ schemas: ['public', 'content', 'auth'] } }, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude({ provider: 'postgresql', datasourceFields:{ schemas: ['public', 'content', 'auth']} })); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should preserve native PostgreSQL enums when schema exists', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'postgresql') { + skip(); + return; + } + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + status UserStatus @default(ACTIVE) + role UserRole @default(USER) +} + +enum UserStatus { + ACTIVE + INACTIVE + SUSPENDED +} + +enum UserRole { + USER + ADMIN + MODERATOR +}`, + { provider: 'postgresql' }, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + const pulledSchema = getSchema(workDir); + + expect(pulledSchema).toEqual(schema); + }); + + it('should not modify schema with PostgreSQL-specific features', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'postgresql') { + skip(); + return; + } + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + status UserStatus @default(ACTIVE) + posts Post[] + metadata Json? + + @@schema('auth') + @@index([status]) +} + +model Post { + id Int @id @default(autoincrement()) + title String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId Int + tags String[] + + @@schema('content') + @@index([authorId]) +} + +enum UserStatus { + ACTIVE + INACTIVE + SUSPENDED +}`, + { provider: 'postgresql', datasourceFields:{ schemas: ['public', 'content', 'auth'] } }, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + + expect(getSchema(workDir)).toEqual(schema); + }); + + it('should restore native type attributes from PostgreSQL typnames', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'postgresql') { + skip(); + return; + } + // PostgreSQL introspection returns typnames like 'int2', 'float8', 'bpchar', + // but Prisma/ZenStack attributes are named @db.SmallInt, @db.DoublePrecision, @db.Char, etc. + // This test verifies the mapping works correctly. + // Note: Default native types (jsonb for Json, bytea for Bytes) are not added when pulling from zero + // because they match the default database type for that field type. + const { workDir } = await createProject( + `model TypeTest { + id Int @id @default(autoincrement()) + smallNumber Int @db.SmallInt() + realNumber Float @db.Real() + doubleNum Float @db.DoublePrecision() + fixedChar String @db.Char(10) + uuid String @db.Uuid() + jsonData Json @db.Json() + jsonbData Json @db.JsonB() + binaryData Bytes @db.ByteA() +}`, + { provider: 'postgresql' }, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + // Remove schema content to simulate restoration from zero + fs.writeFileSync(schemaFile, getDefaultPrelude({ provider: 'postgresql' })); + + // Pull should restore non-default native type attributes + // Default types (jsonb for Json, bytea for Bytes) are not added + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + // Verify key native type mappings are restored correctly: + // - @db.SmallInt for int2 (non-default for Int which defaults to integer/int4) + // - @db.Real for float4 (non-default for Float which defaults to double precision/float8) + // - @db.Char(10) for bpchar with length (non-default for String which defaults to text) + // - @db.Uuid for uuid (non-default for String which defaults to text) + // - @db.Json for json (non-default for Json which defaults to jsonb) + expect(restoredSchema).toContain('@db.SmallInt'); + expect(restoredSchema).toContain('@db.Real'); + expect(restoredSchema).toContain('@db.Char(10)'); + expect(restoredSchema).toContain('@db.Uuid'); + expect(restoredSchema).toContain('@db.Json'); + // Default types should NOT be added when pulling from zero + expect(restoredSchema).not.toContain('@db.Integer'); // integer is default for Int + expect(restoredSchema).not.toContain('@db.DoublePrecision'); // double precision is default for Float + expect(restoredSchema).not.toContain('@db.JsonB'); // jsonb is default for Json + expect(restoredSchema).not.toContain('@db.ByteA'); // bytea is default for Bytes + }); + + it('should correctly map composite foreign key columns by position', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'postgresql') { + skip(); + return; + } + // Composite FK: (tenantId, authorId) REFERENCES Tenant(tenantId, userId) + // The introspection must correlate by position, not match each source column + // to every target column. Without the fix, tenantId would incorrectly map to + // both tenantId AND userId in the target table. + const { workDir, schema } = await createProject( + `model Post { + id Int @id @default(autoincrement()) + title String + tenant Tenant @relation(fields: [tenantId, authorId], references: [tenantId, userId], onDelete: Cascade) + tenantId Int + authorId Int + + @@index([tenantId, authorId]) +} + +model Tenant { + tenantId Int + userId Int + name String + posts Post[] + + @@id([tenantId, userId]) +}`, + { provider: 'postgresql' }, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude({ provider: 'postgresql' })); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should pull stored generated columns as Unsupported with full expression', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'postgresql') { + skip(); + return; + } + // PostgreSQL supports GENERATED ALWAYS AS (expr) STORED since PG 12. + // The introspection should include the full generation expression in the + // datatype so it is rendered as Unsupported("type GENERATED ALWAYS AS (expr) STORED"). + + // 1. Create a project with a base table (we need the DB to exist first) + const { workDir } = await createProject( + `model ComputedUsers { + id Int @id @default(autoincrement()) + firstName String + lastName String +}`, + { provider: 'postgresql' }, + ); + runCli('db push', workDir); + + // 2. Add a generated column via raw SQL (can't be defined in ZModel) + const { Client } = await import('pg'); + const dbName = getTestDbName('postgresql'); + const client = new Client({ connectionString: getTestDbUrl('postgresql', dbName) }); + await client.connect(); + try { + await client.query( + `ALTER TABLE "ComputedUsers" ADD COLUMN "fullName" text GENERATED ALWAYS AS ("firstName" || ' ' || "lastName") STORED` + ); + } finally { + await client.end(); + } + + // 3. Pull from zero + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + fs.writeFileSync(schemaFile, getDefaultPrelude({ provider: 'postgresql' })); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + + // The generated column should be pulled as Unsupported with the full expression. + // format_type returns 'text', and pg_get_expr returns the expression. + expect(restoredSchema).toEqual(await formatDocument(`${getDefaultPrelude({ provider: 'postgresql' })} + +model ComputedUsers { + id Int @id @default(autoincrement()) + firstName String + lastName String + fullName Unsupported('text GENERATED ALWAYS AS ((("firstName" || \\' \\'::text) || "lastName")) STORED')? +}`)); + }); + + it('should pull virtual generated columns as Unsupported with full expression', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'postgresql') { + skip(); + return; + } + // PostgreSQL 17+ supports VIRTUAL generated columns. + // For earlier versions, only STORED is supported, so this test may need to be + // adapted. We test STORED here since it's universally supported. + + const { workDir } = await createProject( + `model ComputedProducts { + id Int @id @default(autoincrement()) + price Int @default(0) + qty Int @default(0) +}`, + { provider: 'postgresql' }, + ); + runCli('db push', workDir); + + const { Client } = await import('pg'); + const dbName = getTestDbName('postgresql'); + const client = new Client({ connectionString: getTestDbUrl('postgresql', dbName) }); + await client.connect(); + try { + await client.query( + `ALTER TABLE "ComputedProducts" ADD COLUMN "total" integer GENERATED ALWAYS AS ("price" * "qty") STORED` + ); + } finally { + await client.end(); + } + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + fs.writeFileSync(schemaFile, getDefaultPrelude({ provider: 'postgresql' })); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + + expect(restoredSchema).toEqual(await formatDocument(`${getDefaultPrelude({ provider: 'postgresql' })} + +model ComputedProducts { + id Int @id @default(autoincrement()) + price Int @default(0) + qty Int @default(0) + total Unsupported('integer GENERATED ALWAYS AS ((price * qty)) STORED')? +}`)); + }); +}); + +describe('DB pull - MySQL specific features', () => { + it('should detect single-column unique indexes via STATISTICS', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'mysql') { + skip(); + return; + } + // MySQL's COLUMN_KEY may not reliably reflect unique indexes in all cases. + // The introspection should also check INFORMATION_SCHEMA.STATISTICS for + // NON_UNIQUE = 0 single-column indexes to correctly detect uniqueness, + // so that the index-processing skip logic (which checks index.unique + + // single-column) doesn't cause a missing @unique attribute. + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + nickname String? @unique +}`, + { provider: 'mysql' }, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + // Pull from zero to test introspection detects unique columns correctly + fs.writeFileSync(schemaFile, getDefaultPrelude({ provider: 'mysql' })); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should pull stored generated columns as Unsupported with full expression', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'mysql') { + skip(); + return; + } + // MySQL supports both VIRTUAL and STORED generated columns. + // The introspection should include the full generation expression in the + // datatype so it is rendered as Unsupported("type GENERATED ALWAYS AS (expr) STORED"). + + // 1. Create a project with a base table (we need the DB to exist first) + const { workDir } = await createProject( + `model ComputedUsers { + id Int @id @default(autoincrement()) + firstName String @db.VarChar(255) + lastName String @db.VarChar(255) +}`, + { provider: 'mysql' }, + ); + runCli('db push', workDir); + + // 2. Add a generated column via raw SQL (can't be defined in ZModel) + const mysql = await import('mysql2/promise'); + const dbName = getTestDbName('mysql'); + const connection = await mysql.createConnection(getTestDbUrl('mysql', dbName)); + try { + await connection.execute( + "ALTER TABLE `ComputedUsers` ADD COLUMN `fullName` varchar(511) GENERATED ALWAYS AS (CONCAT(`firstName`, ' ', `lastName`)) STORED" + ); + } finally { + await connection.end(); + } + + // 3. Pull from zero + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + fs.writeFileSync(schemaFile, getDefaultPrelude({ provider: 'mysql' })); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + + // The generated column should be pulled as Unsupported with the full expression. + // MySQL uses COLUMN_TYPE (e.g., 'varchar(511)') and GENERATION_EXPRESSION for the expr, + // and EXTRA contains 'STORED GENERATED' or 'VIRTUAL GENERATED'. + expect(restoredSchema).toEqual(await formatDocument(`${getDefaultPrelude({ provider: 'mysql' })} + +model ComputedUsers { + id Int @id @default(autoincrement()) + firstName String @db.VarChar(255) + lastName String @db.VarChar(255) + fullName Unsupported('varchar(511) GENERATED ALWAYS AS (concat(\`firstName\`,\\' \\',\`lastName\`)) STORED')? +}`)); + }); + + it('should pull virtual generated columns as Unsupported with full expression', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'mysql') { + skip(); + return; + } + + const { workDir } = await createProject( + `model ComputedProducts { + id Int @id @default(autoincrement()) + price Int @default(0) + qty Int @default(0) +}`, + { provider: 'mysql' }, + ); + runCli('db push', workDir); + + const mysql = await import('mysql2/promise'); + const dbName = getTestDbName('mysql'); + const connection = await mysql.createConnection(getTestDbUrl('mysql', dbName)); + try { + await connection.execute( + "ALTER TABLE `ComputedProducts` ADD COLUMN `total` int GENERATED ALWAYS AS (`price` * `qty`) VIRTUAL" + ); + } finally { + await connection.end(); + } + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + fs.writeFileSync(schemaFile, getDefaultPrelude({ provider: 'mysql' })); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + + expect(restoredSchema).toEqual(await formatDocument(`${getDefaultPrelude({ provider: 'mysql' })} + +model ComputedProducts { + id Int @id @default(autoincrement()) + price Int @default(0) + qty Int @default(0) + total Unsupported('int GENERATED ALWAYS AS ((\`price\` * \`qty\`)) VIRTUAL')? +}`)); + }); +}); + +describe('DB pull - SQLite specific features', () => { + it('should restore composite foreign key relations', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'sqlite') { + skip(); + return; + } + // Composite FK: (tenantId, authorId) REFERENCES Tenant(tenantId, userId). + // The SQLite introspection extracts FK constraint names by parsing the + // CREATE TABLE DDL. The current regex only captures a single column inside + // FOREIGN KEY(...), so composite FK constraint names are lost. Without a + // constraint name, the downstream relation grouping (pull/index.ts) skips + // the FK columns entirely and the relation is not restored. + const { workDir, schema } = await createProject( + `model Post { + id Int @id @default(autoincrement()) + title String + tenant Tenant @relation(fields: [tenantId, authorId], references: [tenantId, userId], onDelete: Cascade) + tenantId Int + authorId Int + + @@index([tenantId, authorId]) +} + +model Tenant { + tenantId Int + userId Int + name String + posts Post[] + + @@id([tenantId, userId]) +}`, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); + + it('should map columns without a declared type to Bytes', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'sqlite') { + skip(); + return; + } + // Create a minimal project and push to get the database file. + const { workDir } = await createProject(""); + + // Open the SQLite database directly and add a table with an untyped column. + // In SQLite, CREATE TABLE t("data") gives column "data" no declared type, + // which per affinity rules means BLOB affinity — should map to Bytes. + const dbPath = path.join(workDir, 'zenstack', 'test.db'); + const SQLite = (await import('better-sqlite3')).default; + const db = new SQLite(dbPath); + db.exec('CREATE TABLE "UntypedTest" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "data")'); + db.close(); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + // The untyped "data" column should be pulled as Bytes (BLOB affinity), + // not as Unsupported. + expect(restoredSchema).toContain('data Bytes?'); + expect(restoredSchema).not.toContain('Unsupported'); + }); + + it('should pull stored generated columns as Unsupported', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'sqlite') { + skip(); + return; + } + // SQLite PRAGMA table_xinfo reports generated columns with hidden values: + // hidden = 2 → VIRTUAL generated column + // hidden = 3 → STORED generated column + // Both types should be pulled as Unsupported("full type definition") + // because generated columns are read-only and cannot be written to. + + const { workDir } = await createProject(''); + + const dbPath = path.join(workDir, 'zenstack', 'test.db'); + const SQLite = (await import('better-sqlite3')).default; + const db = new SQLite(dbPath); + db.exec(` + CREATE TABLE "ComputedUsers" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "fullName" TEXT GENERATED ALWAYS AS (firstName || ' ' || lastName) STORED + ) + `); + db.close(); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + + // first_name and last_name should be regular String fields + expect(restoredSchema).toEqual(await formatDocument(`${getDefaultPrelude()} + +model ComputedUsers { + id Int @id @default(autoincrement()) + firstName String + lastName String + fullName Unsupported('TEXT GENERATED ALWAYS AS (firstName || \\' \\' || lastName) STORED')? +}`)); + }); + + it('should pull virtual generated columns as Unsupported', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'sqlite') { + skip(); + return; + } + + const { workDir } = await createProject(''); + + const dbPath = path.join(workDir, 'zenstack', 'test.db'); + const SQLite = (await import('better-sqlite3')).default; + const db = new SQLite(dbPath); + db.exec(` + CREATE TABLE "ComputedProducts" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "price" INTEGER NOT NULL DEFAULT 0, + "qty" INTEGER NOT NULL DEFAULT 0, + "total" INTEGER GENERATED ALWAYS AS ("price" * "qty") VIRTUAL + ) + `); + db.close(); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + + expect(restoredSchema).toEqual(await formatDocument(`${getDefaultPrelude()} + +model ComputedProducts { + id Int @id @default(autoincrement()) + price Int @default(0) + qty Int @default(0) + total Unsupported('INTEGER GENERATED ALWAYS AS ("price" * "qty") VIRTUAL')? +}`)); + }); +}); + +describe('DB pull - SQL specific features', () => { + it('should restore enum fields from zero', async ({ skip }) => { + const provider = getTestDbProvider(); + if (provider !== 'mysql' && provider !== 'postgresql') { + skip(); + return; + } + + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique + status UserStatus @default(ACTIVE) +} + +enum UserStatus { + ACTIVE + INACTIVE + SUSPENDED +}`); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + // Remove schema content to simulate restoration from zero + fs.writeFileSync(schemaFile, getDefaultPrelude()); + + // Pull should fully restore the schema including enum fields + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + expect(restoredSchema).toEqual(schema); + }); +}); diff --git a/packages/cli/test/db/push.test.ts b/packages/cli/test/db/push.test.ts new file mode 100644 index 000000000..bba9e05bd --- /dev/null +++ b/packages/cli/test/db/push.test.ts @@ -0,0 +1,18 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createProject, runCli } from '../utils'; + +const model = ` +model User { + id String @id @default(cuid()) +} +`; + +describe('CLI db commands test', () => { + it('should generate a database with db push', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); + runCli('db push', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/test.db'))).toBe(true); + }); +}); diff --git a/packages/cli/test/format.test.ts b/packages/cli/test/format.test.ts index 9c95960a3..0bfb32d7f 100644 --- a/packages/cli/test/format.test.ts +++ b/packages/cli/test/format.test.ts @@ -10,8 +10,8 @@ model User { `; describe('CLI format command test', () => { - it('should format a valid schema successfully', () => { - const workDir = createProject(model); + it('should format a valid schema successfully', async () => { + const { workDir } = await createProject(model); expect(() => runCli('format', workDir)).not.toThrow(); const updatedContent = fs.readFileSync(`${workDir}/zenstack/schema.zmodel`, 'utf-8'); expect( @@ -22,12 +22,12 @@ describe('CLI format command test', () => { ).toBeTruthy(); }); - it('should silently ignore invalid schema', () => { + it('should silently ignore invalid schema', async () => { const invalidModel = ` model User { id String @id @default(cuid()) `; - const workDir = createProject(invalidModel); + const { workDir } = await createProject(invalidModel); expect(() => runCli('format', workDir)).not.toThrow(); }); }); diff --git a/packages/cli/test/generate.test.ts b/packages/cli/test/generate.test.ts index 074e88e56..6b270b4a8 100644 --- a/packages/cli/test/generate.test.ts +++ b/packages/cli/test/generate.test.ts @@ -10,28 +10,28 @@ model User { `; describe('CLI generate command test', () => { - it('should generate a TypeScript schema', () => { - const workDir = createProject(model); + it('should generate a TypeScript schema', async () => { + const { workDir } = await createProject(model); runCli('generate', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(false); }); - it('should respect custom output directory', () => { - const workDir = createProject(model); + it('should respect custom output directory', async () => { + const { workDir } = await createProject(model); runCli('generate --output ./zen', workDir); expect(fs.existsSync(path.join(workDir, 'zen/schema.ts'))).toBe(true); }); - it('should respect custom schema location', () => { - const workDir = createProject(model); + it('should respect custom schema location', async () => { + const { workDir } = await createProject(model); fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/foo.zmodel')); runCli('generate --schema ./zenstack/foo.zmodel', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); }); - it('should respect package.json config', () => { - const workDir = createProject(model); + it('should respect package.json config', async () => { + const { workDir } = await createProject(model); fs.mkdirSync(path.join(workDir, 'foo')); fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel')); fs.rmdirSync(path.join(workDir, 'zenstack')); @@ -45,8 +45,8 @@ describe('CLI generate command test', () => { expect(fs.existsSync(path.join(workDir, 'bar/schema.ts'))).toBe(true); }); - it('should respect package.json schema dir config', () => { - const workDir = createProject(model); + it('should respect package.json schema dir config', async () => { + const { workDir } = await createProject(model); fs.mkdirSync(path.join(workDir, 'foo')); fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'foo/schema.zmodel')); fs.rmdirSync(path.join(workDir, 'zenstack')); @@ -60,15 +60,15 @@ describe('CLI generate command test', () => { expect(fs.existsSync(path.join(workDir, 'bar/schema.ts'))).toBe(true); }); - it('should respect lite option', () => { - const workDir = createProject(model); + it('should respect lite option', async () => { + const { workDir } = await createProject(model); runCli('generate --lite', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(true); }); - it('should respect liteOnly option', () => { - const workDir = createProject(model); + it('should respect liteOnly option', async () => { + const { workDir } = await createProject(model); runCli('generate --lite-only', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(false); expect(fs.existsSync(path.join(workDir, 'zenstack/schema-lite.ts'))).toBe(true); diff --git a/packages/cli/test/migrate.test.ts b/packages/cli/test/migrate.test.ts index 56a0fec83..bb3a7cd53 100644 --- a/packages/cli/test/migrate.test.ts +++ b/packages/cli/test/migrate.test.ts @@ -10,37 +10,37 @@ model User { `; describe('CLI migrate commands test', () => { - it('should generate a database with migrate dev', () => { - const workDir = createProject(model); + it('should generate a database with migrate dev', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); runCli('migrate dev --name init', workDir); - expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); + expect(fs.existsSync(path.join(workDir, 'zenstack/test.db'))).toBe(true); expect(fs.existsSync(path.join(workDir, 'zenstack/migrations'))).toBe(true); }); - it('should reset the database with migrate reset', () => { - const workDir = createProject(model); + it('should reset the database with migrate reset', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); runCli('db push', workDir); - expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); + expect(fs.existsSync(path.join(workDir, 'zenstack/test.db'))).toBe(true); runCli('migrate reset --force', workDir); - expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); + expect(fs.existsSync(path.join(workDir, 'zenstack/test.db'))).toBe(true); }); - it('should reset the database with migrate deploy', () => { - const workDir = createProject(model); + it('should reset the database with migrate deploy', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); runCli('migrate dev --name init', workDir); - fs.rmSync(path.join(workDir, 'zenstack/dev.db')); + fs.rmSync(path.join(workDir, 'zenstack/test.db')); runCli('migrate deploy', workDir); - expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); + expect(fs.existsSync(path.join(workDir, 'zenstack/test.db'))).toBe(true); }); - it('supports migrate status', () => { - const workDir = createProject(model); + it('supports migrate status', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); runCli('migrate dev --name init', workDir); runCli('migrate status', workDir); }); - it('supports migrate resolve', () => { - const workDir = createProject(model); + it('supports migrate resolve', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); runCli('migrate dev --name init', workDir); // find the migration record "timestamp_init" @@ -51,7 +51,7 @@ describe('CLI migrate commands test', () => { fs.writeFileSync(path.join(workDir, 'zenstack/migrations', migration!, 'migration.sql'), 'invalid content'); // redeploy the migration, which will fail - fs.rmSync(path.join(workDir, 'zenstack/dev.db'), { force: true }); + fs.rmSync(path.join(workDir, 'zenstack/test.db'), { force: true }); try { runCli('migrate deploy', workDir); } catch { @@ -65,8 +65,8 @@ describe('CLI migrate commands test', () => { runCli(`migrate resolve --applied ${migration}`, workDir); }); - it('should throw error when neither applied nor rolled-back is provided', () => { - const workDir = createProject(model); + it('should throw error when neither applied nor rolled-back is provided', async () => { + const { workDir } = await createProject(model, { provider: 'sqlite' }); expect(() => runCli('migrate resolve', workDir)).toThrow(); }); }); diff --git a/packages/cli/test/plugins/custom-plugin.test.ts b/packages/cli/test/plugins/custom-plugin.test.ts index 084bf9cd7..3492dbbe6 100644 --- a/packages/cli/test/plugins/custom-plugin.test.ts +++ b/packages/cli/test/plugins/custom-plugin.test.ts @@ -5,8 +5,8 @@ import { createProject, runCli } from '../utils'; import { execSync } from 'node:child_process'; describe('Custom plugins tests', () => { - it('runs custom plugin generator', () => { - const workDir = createProject(` + it('runs custom plugin generator', async () => { + const { workDir } = await createProject(` plugin custom { provider = '../my-plugin.js' output = '../custom-output' diff --git a/packages/cli/test/plugins/prisma-plugin.test.ts b/packages/cli/test/plugins/prisma-plugin.test.ts index 739252e21..4b619c679 100644 --- a/packages/cli/test/plugins/prisma-plugin.test.ts +++ b/packages/cli/test/plugins/prisma-plugin.test.ts @@ -4,8 +4,8 @@ import { describe, expect, it } from 'vitest'; import { createProject, runCli } from '../utils'; describe('Core plugins tests', () => { - it('can automatically generate a TypeScript schema with default output', () => { - const workDir = createProject(` + it('can automatically generate a TypeScript schema with default output', async () => { + const { workDir } = await createProject(` model User { id String @id @default(cuid()) } @@ -14,8 +14,8 @@ model User { expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); }); - it('can automatically generate a TypeScript schema with custom output', () => { - const workDir = createProject(` + it('can automatically generate a TypeScript schema with custom output', async () => { + const { workDir } = await createProject(` plugin typescript { provider = '@core/typescript' output = '../generated-schema' @@ -29,8 +29,8 @@ model User { expect(fs.existsSync(path.join(workDir, 'generated-schema/schema.ts'))).toBe(true); }); - it('can generate a Prisma schema with default output', () => { - const workDir = createProject(` + it('can generate a Prisma schema with default output', async () => { + const { workDir } = await createProject(` plugin prisma { provider = '@core/prisma' } @@ -43,8 +43,8 @@ model User { expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true); }); - it('can generate a Prisma schema with custom output', () => { - const workDir = createProject(` + it('can generate a Prisma schema with custom output', async () => { + const { workDir } = await createProject(` plugin prisma { provider = '@core/prisma' output = '../prisma/schema.prisma' @@ -58,8 +58,8 @@ model User { expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true); }); - it('can generate a Prisma schema with custom output relative to zenstack.output', () => { - const workDir = createProject(` + it('can generate a Prisma schema with custom output relative to zenstack.output', async () => { + const { workDir } = await createProject(` plugin prisma { provider = '@core/prisma' output = './schema.prisma' diff --git a/packages/cli/test/utils.ts b/packages/cli/test/utils.ts index 2fafb2074..31a86dfb9 100644 --- a/packages/cli/test/utils.ts +++ b/packages/cli/test/utils.ts @@ -1,20 +1,107 @@ -import { createTestProject } from '@zenstackhq/testtools'; +import { createTestProject, getTestDbProvider } from '@zenstackhq/testtools'; +import { createHash } from 'node:crypto'; import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +import { expect } from 'vitest'; +import { formatDocument } from '@zenstackhq/language'; -const ZMODEL_PRELUDE = `datasource db { - provider = "sqlite" - url = "file:./dev.db" +const TEST_PG_CONFIG = { + host: process.env['TEST_PG_HOST'] ?? 'localhost', + 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', +}; + +const TEST_MYSQL_CONFIG = { + host: process.env['TEST_MYSQL_HOST'] ?? 'localhost', + port: process.env['TEST_MYSQL_PORT'] ? parseInt(process.env['TEST_MYSQL_PORT']) : 3306, + user: process.env['TEST_MYSQL_USER'] ?? 'root', + password: process.env['TEST_MYSQL_PASSWORD'] ?? 'mysql', +}; + +export function getTestDbName(provider: string) { + if (provider === 'sqlite') { + return './test.db'; + } + const testName = expect.getState().currentTestName ?? 'unnamed'; + const testPath = expect.getState().testPath ?? ''; + // digest test name + const digest = createHash('md5') + .update(testName + testPath) + .digest('hex'); + // compute a database name based on test name + return ( + 'test_' + + testName + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/_+/g, '_') + .substring(0, 30) + + digest.slice(0, 6) + ); +} + +export function getTestDbUrl(provider: 'sqlite' | 'postgresql' | 'mysql', dbName: string): string { + switch (provider) { + case 'sqlite': + return `file:${dbName}`; + case 'postgresql': + return `postgres://${TEST_PG_CONFIG.user}:${TEST_PG_CONFIG.password}@${TEST_PG_CONFIG.host}:${TEST_PG_CONFIG.port}/${dbName}`; + case 'mysql': + return `mysql://${TEST_MYSQL_CONFIG.user}:${TEST_MYSQL_CONFIG.password}@${TEST_MYSQL_CONFIG.host}:${TEST_MYSQL_CONFIG.port}/${dbName}`; + default: + throw new Error(`Unsupported provider: ${provider}`); + } +} + +export function getDefaultPrelude(options?: { provider?: 'sqlite' | 'postgresql' | 'mysql', datasourceFields?: Record }) { + const provider = (options?.provider || getTestDbProvider()) ?? 'sqlite'; + const dbName = getTestDbName(provider); + let dbUrl: string; + + switch (provider) { + case 'sqlite': + dbUrl = `file:${dbName}`; + break; + case 'postgresql': + dbUrl = `postgres://${TEST_PG_CONFIG.user}:${TEST_PG_CONFIG.password}@${TEST_PG_CONFIG.host}:${TEST_PG_CONFIG.port}/${dbName}`; + break; + case 'mysql': + dbUrl = `mysql://${TEST_MYSQL_CONFIG.user}:${TEST_MYSQL_CONFIG.password}@${TEST_MYSQL_CONFIG.host}:${TEST_MYSQL_CONFIG.port}/${dbName}`; + break; + default: + throw new Error(`Unsupported provider: ${provider}`); + } + // Build fields array for proper alignment (matching ZModelCodeGenerator) + const fields: [string, string][] = [ + ['provider', `'${provider}'`], + ['url', `'${dbUrl}'`], + ...Object.entries(options?.datasourceFields || {}).map(([k, v]) => { + const value = Array.isArray(v) ? `[${v.map(item => `'${item}'`).join(', ')}]` : `'${v}'`; + return [k, value] as [string, string]; + }), + ]; + + const formattedFields = fields.map(([name, value]) => { + return ` ${name} = ${value}`; + }).join('\n'); + + const ZMODEL_PRELUDE = `datasource db {\n${formattedFields}\n}`; + return ZMODEL_PRELUDE; } -`; -export function createProject(zmodel: string, addPrelude = true) { +export async function createProject( + zmodel: string, + options?: { customPrelude?: boolean; provider?: 'sqlite' | 'postgresql' | 'mysql'; datasourceFields?: Record }, +) { const workDir = createTestProject(); fs.mkdirSync(path.join(workDir, 'zenstack'), { recursive: true }); const schemaPath = path.join(workDir, 'zenstack/schema.zmodel'); - fs.writeFileSync(schemaPath, addPrelude ? `${ZMODEL_PRELUDE}\n\n${zmodel}` : zmodel); - return workDir; + const content = options?.customPrelude ? zmodel : `${getDefaultPrelude({ provider: options?.provider, datasourceFields: options?.datasourceFields })}\n\n${zmodel}`; + const schema = await formatDocument(content); + fs.writeFileSync(schemaPath, schema); + return { workDir, schema }; } export function runCli(command: string, cwd: string) { diff --git a/packages/language/package.json b/packages/language/package.json index f5bef4ac3..5d809e7a1 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -49,6 +49,16 @@ "default": "./dist/utils.cjs" } }, + "./factory": { + "import": { + "types": "./dist/factory.d.ts", + "default": "./dist/factory.js" + }, + "require": { + "types": "./dist/factory.d.cts", + "default": "./dist/factory.cjs" + } + }, "./package.json": { "import": "./package.json", "require": "./package.json" diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index 4f473ed78..cb604c74a 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -120,7 +120,7 @@ function dbgenerated(expr: String?): Any { /** * Checks if the field value contains the search string. By default, the search is case-sensitive, and * "LIKE" operator is used to match. If `caseInSensitive` is true, "ILIKE" operator is used if - * supported, otherwise it still falls back to "LIKE" and delivers whatever the database's + * supported, otherwise it still falls back to "LIKE" and delivers whatever the database's * behavior is. */ function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean { @@ -135,7 +135,7 @@ function contains(field: String, search: String, caseInSensitive: Boolean?): Boo /** * Checks the field value starts with the search string. By default, the search is case-sensitive, and * "LIKE" operator is used to match. If `caseInSensitive` is true, "ILIKE" operator is used if - * supported, otherwise it still falls back to "LIKE" and delivers whatever the database's + * supported, otherwise it still falls back to "LIKE" and delivers whatever the database's * behavior is. */ function startsWith(field: String, search: String, caseInSensitive: Boolean?): Boolean { @@ -144,7 +144,7 @@ function startsWith(field: String, search: String, caseInSensitive: Boolean?): B /** * Checks if the field value ends with the search string. By default, the search is case-sensitive, and * "LIKE" operator is used to match. If `caseInSensitive` is true, "ILIKE" operator is used if - * supported, otherwise it still falls back to "LIKE" and delivers whatever the database's + * supported, otherwise it still falls back to "LIKE" and delivers whatever the database's * behavior is. */ function endsWith(field: String, search: String, caseInSensitive: Boolean?): Boolean { diff --git a/packages/language/src/document.ts b/packages/language/src/document.ts index 9642e61d5..7426c606d 100644 --- a/packages/language/src/document.ts +++ b/packages/language/src/document.ts @@ -13,7 +13,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { isDataModel, isDataSource, type Model } from './ast'; import { DB_PROVIDERS_SUPPORTING_LIST_TYPE, STD_LIB_MODULE_NAME } from './constants'; -import { createZModelServices } from './module'; +import { createZModelServices, type ZModelServices } from './module'; import { getAllFields, getDataModelAndTypeDefs, @@ -32,8 +32,10 @@ import type { ZModelFormatter } from './zmodel-formatter'; export async function loadDocument( fileName: string, additionalModelFiles: string[] = [], + mergeImports: boolean = true, ): Promise< - { success: true; model: Model; warnings: string[] } | { success: false; errors: string[]; warnings: string[] } + | { success: true; model: Model; warnings: string[]; services: ZModelServices } + | { success: false; errors: string[]; warnings: string[] } > { const { ZModelLanguage: services } = createZModelServices(false); const extensions = services.LanguageMetaData.fileExtensions; @@ -121,17 +123,19 @@ export async function loadDocument( const model = document.parseResult.value as Model; - // merge all declarations into the main document - const imported = mergeImportsDeclarations(langiumDocuments, model); + if (mergeImports) { + // merge all declarations into the main document + const imported = mergeImportsDeclarations(langiumDocuments, model); - // remove imported documents - imported.forEach((model) => { - langiumDocuments.deleteDocument(model.$document!.uri); - services.shared.workspace.IndexManager.remove(model.$document!.uri); - }); + // remove imported documents + imported.forEach((model) => { + langiumDocuments.deleteDocument(model.$document!.uri); + services.shared.workspace.IndexManager.remove(model.$document!.uri); + }); + } // extra validation after merging imported declarations - const additionalErrors = validationAfterImportMerge(model); + const additionalErrors = mergeImports === true ? validationAfterImportMerge(model) : []; if (additionalErrors.length > 0) { return { success: false, @@ -143,6 +147,7 @@ export async function loadDocument( return { success: true, model: document.parseResult.value as Model, + services, warnings, }; } diff --git a/packages/language/src/factory/ast-factory.ts b/packages/language/src/factory/ast-factory.ts new file mode 100644 index 000000000..7618f0738 --- /dev/null +++ b/packages/language/src/factory/ast-factory.ts @@ -0,0 +1,52 @@ +import { type AstNode } from '../ast'; + +export type ContainerProps = { + $container: T; + $containerProperty?: string; + $containerIndex?: number; +}; + +type NodeFactoriesFor = { + [K in keyof N as {} extends Pick ? never : K]: N[K] extends (infer U)[] + ? (AstFactory | U)[] + : AstFactory | N[K]; +} & { + [K in keyof N as {} extends Pick ? K : never]?: N[K] extends (infer U)[] + ? (AstFactory | U)[] + : AstFactory | N[K]; +}; + +export abstract class AstFactory { + node = {} as T; + constructor({ type, node }: { type: T['$type']; node?: Partial }) { + (this.node as any).$type = type; + if (node) { + this.update(node); + } + } + setContainer(container: T['$container']) { + (this.node as any).$container = container; + return this; + } + + get(params?: ContainerProps): T { + if (params) this.update(params as any); + return this.node; + } + update(nodeArg: Partial>): T { + const keys = Object.keys(nodeArg as object); + keys.forEach((key) => { + const child = (nodeArg as any)[key]; + if (child instanceof AstFactory) { + (this.node as any)[key] = child.get({ $container: this.node as any }); + } else if (Array.isArray(child)) { + (this.node as any)[key] = child.map((item: any) => + item instanceof AstFactory ? item.get({ $container: this.node as any }) : item, + ); + } else { + (this.node as any)[key] = child; + } + }); + return this.node; + } +} diff --git a/packages/language/src/factory/attribute.ts b/packages/language/src/factory/attribute.ts new file mode 100644 index 000000000..b59e35ef1 --- /dev/null +++ b/packages/language/src/factory/attribute.ts @@ -0,0 +1,281 @@ +import { AstFactory } from './ast-factory'; +import { + Attribute, + AttributeArg, + AttributeParam, + AttributeParamType, + DataFieldAttribute, + DataModelAttribute, + Expression, + InternalAttribute, + TypeDeclaration, + type Reference, + type RegularID, +} from '../ast'; +import { ExpressionBuilder } from './expression'; + +export class DataFieldAttributeFactory extends AstFactory { + args: AttributeArgFactory[] = []; + decl?: Reference; + constructor() { + super({ type: DataFieldAttribute, node: { args: [] } }); + } + setDecl(decl: Attribute) { + if (!decl) { + throw new Error('Attribute declaration is required'); + } + this.decl = { + $refText: decl.name, + ref: decl, + }; + this.update({ + decl: this.decl, + }); + return this; + } + addArg(builder: (b: ExpressionBuilder) => AstFactory, name?: string) { + const factory = new AttributeArgFactory().setValue(builder); + if (name) { + factory.setName(name); + } + this.args.push(factory); + this.update({ + args: this.args, + }); + return this; + } +} + +export class DataModelAttributeFactory extends AstFactory { + args: AttributeArgFactory[] = []; + decl?: Reference; + constructor() { + super({ type: DataModelAttribute, node: { args: [] } }); + } + setDecl(decl: Attribute) { + if (!decl) { + throw new Error('Attribute declaration is required'); + } + this.decl = { + $refText: decl.name, + ref: decl, + }; + this.update({ + decl: this.decl, + }); + return this; + } + addArg(builder: (b: ExpressionBuilder) => AstFactory, name?: string) { + const factory = new AttributeArgFactory().setValue(builder); + if (name) { + factory.setName(name); + } + this.args.push(factory); + this.update({ + args: this.args, + }); + return this; + } +} + +export class AttributeArgFactory extends AstFactory { + name?: RegularID = ''; + value?: AstFactory; + + constructor() { + super({ type: AttributeArg }); + } + + setName(name: RegularID) { + this.name = name; + this.update({ + name: this.name, + }); + return this; + } + + setValue(builder: (b: ExpressionBuilder) => AstFactory) { + this.value = builder(ExpressionBuilder()); + this.update({ + value: this.value, + }); + return this; + } +} + +export class InternalAttributeFactory extends AstFactory { + decl?: Reference; + args: AttributeArgFactory[] = []; + + constructor() { + super({ type: InternalAttribute, node: { args: [] } }); + } + + setDecl(decl: Attribute) { + this.decl = { + $refText: decl.name, + ref: decl, + }; + this.update({ + decl: this.decl, + }); + return this; + } + + addArg(builder: (b: ExpressionBuilder) => AstFactory, name?: string) { + const factory = new AttributeArgFactory().setValue(builder); + if (name) { + factory.setName(name); + } + this.args.push(factory); + this.update({ + args: this.args, + }); + return this; + } +} + +export class AttributeParamFactory extends AstFactory { + attributes: InternalAttributeFactory[] = []; + comments: string[] = []; + default?: boolean; + name?: RegularID; + type?: AttributeParamTypeFactory; + + constructor() { + super({ + type: AttributeParam, + node: { + comments: [], + attributes: [], + }, + }); + } + + addAttribute(builder: (b: InternalAttributeFactory) => InternalAttributeFactory) { + this.attributes.push(builder(new InternalAttributeFactory())); + this.update({ + attributes: this.attributes, + }); + return this; + } + + setComments(comments: string[]) { + this.comments = comments; + this.update({ + comments: this.comments, + }); + return this; + } + + setDefault(defaultValue: boolean) { + this.default = defaultValue; + this.update({ + default: this.default, + }); + return this; + } + + setName(name: string) { + this.name = name; + this.update({ + name: this.name, + }); + return this; + } + + setType(builder: (b: AttributeParamTypeFactory) => AttributeParamTypeFactory) { + this.type = builder(new AttributeParamTypeFactory()); + this.update({ + type: this.type, + }); + return this; + } +} + +export class AttributeParamTypeFactory extends AstFactory { + array?: boolean; + optional?: boolean; + reference?: Reference; + type?: AttributeParamType['type']; + constructor() { + super({ type: AttributeParamType }); + } + setArray(array: boolean) { + this.array = array; + this.update({ + array: this.array, + }); + return this; + } + + setOptional(optional: boolean) { + this.optional = optional; + this.update({ + optional: this.optional, + }); + return this; + } + + setReference(reference: TypeDeclaration) { + this.reference = { + $refText: reference.name, + ref: reference, + }; + this.update({ + reference: this.reference, + }); + return this; + } + + setType(type: AttributeParamType['type']) { + this.type = type; + this.update({ + type: this.type, + }); + return this; + } +} + +export class AttributeFactory extends AstFactory { + name?: string; + comments: string[] = []; + attributes: InternalAttributeFactory[] = []; + params: AttributeParamFactory[] = []; + + constructor() { + super({ type: Attribute, node: { comments: [], attributes: [], params: [] } }); + } + + setName(name: string) { + this.name = name; + this.update({ + name: this.name, + }); + return this; + } + + setComments(comments: string[]) { + this.comments = comments; + this.update({ + comments: this.comments, + }); + return this; + } + + addAttribute(builder: (b: InternalAttributeFactory) => InternalAttributeFactory) { + this.attributes.push(builder(new InternalAttributeFactory())); + this.update({ + attributes: this.attributes, + }); + return this; + } + + addParam(builder: (b: AttributeParamFactory) => AttributeParamFactory) { + this.params.push(builder(new AttributeParamFactory())); + this.update({ + params: this.params, + }); + return this; + } +} diff --git a/packages/language/src/factory/declaration.ts b/packages/language/src/factory/declaration.ts new file mode 100644 index 000000000..a6f772a20 --- /dev/null +++ b/packages/language/src/factory/declaration.ts @@ -0,0 +1,373 @@ +import { AstFactory } from './ast-factory'; +import { AbstractDeclaration, type Reference } from '../ast'; +import { + type BuiltinType, + DataField, + DataFieldType, + DataModel, + Enum, + EnumField, + LiteralExpr, + Model, + ModelImport, + type RegularID, + type RegularIDWithTypeNames, + TypeDeclaration, + type TypeDef, + UnsupportedFieldType, +} from '../generated/ast'; +import { AttributeFactory, DataFieldAttributeFactory, DataModelAttributeFactory } from './attribute'; +import { ExpressionBuilder } from './expression'; +export const DeclarationBuilder = () => + ({ + get Attribute() { + return new AttributeFactory(); + }, + get DataModel() { + return new DataModelFactory(); + }, + get DataSource(): any { + throw new Error('DataSource is not implemented'); + }, + get Enum() { + return new EnumFactory(); + }, + get FunctionDecl(): any { + throw new Error('FunctionDecl is not implemented'); + }, + get GeneratorDecl(): any { + throw new Error('GeneratorDecl is not implemented'); + }, + get Plugin(): any { + throw new Error('Plugin is not implemented'); + }, + get Procedure(): any { + throw new Error('Procedure is not implemented'); + }, + get TypeDef(): any { + throw new Error('TypeDef is not implemented'); + }, + }) satisfies DeclarationBuilderType; +type DeclarationBuilderType = { + [K in T['$type']]: AstFactory>; +}; +type DeclarationBuilderMap = ReturnType; + +export type DeclarationBuilder = Pick< + DeclarationBuilderMap, + Extract +>; + +export class DataModelFactory extends AstFactory { + attributes: DataModelAttributeFactory[] = []; + baseModel?: Reference; + comments: string[] = []; + fields: DataFieldFactory[] = []; + isView?: boolean; + mixins: Reference[] = []; + name?: RegularID; + + constructor() { + super({ + type: DataModel, + node: { + attributes: [], + comments: [], + fields: [], + mixins: [], + }, + }); + } + + addAttribute(builder: (attr: DataModelAttributeFactory) => DataModelAttributeFactory) { + this.attributes.push(builder(new DataModelAttributeFactory()).setContainer(this.node)); + this.update({ + attributes: this.attributes, + }); + return this; + } + + setBaseModel(model: Reference) { + this.baseModel = model; + this.update({ + baseModel: this.baseModel, + }); + return this; + } + + setComments(comments: string[]) { + this.comments = comments; + this.update({ + comments: this.comments, + }); + return this; + } + + addComment(comment: string) { + this.comments.push(comment); + this.update({ + comments: this.comments, + }); + return this; + } + + addField(builder: (field: DataFieldFactory) => DataFieldFactory) { + this.fields.push(builder(new DataFieldFactory()).setContainer(this.node)); + this.update({ + fields: this.fields, + }); + return this; + } + + setIsView(isView: boolean) { + this.isView = isView; + this.update({ + isView: this.isView, + }); + return this; + } + + addMixin(mixin: Reference) { + this.mixins.push(mixin); + this.update({ + mixins: this.mixins, + }); + return this; + } + + setName(name: string) { + this.name = name; + this.update({ + name: this.name, + }); + return this; + } +} + +export class DataFieldFactory extends AstFactory { + attributes: DataFieldAttributeFactory[] = []; + comments: string[] = []; + name?: string; + type?: DataFieldTypeFactory; + + constructor() { + super({ type: DataField, node: { attributes: [], comments: [] } }); + } + + addAttribute( + builder: ((attr: DataFieldAttributeFactory) => DataFieldAttributeFactory) | DataFieldAttributeFactory, + ) { + if (builder instanceof DataFieldAttributeFactory) { + builder.setContainer(this.node); + this.attributes.push(builder); + } else { + const attr = builder(new DataFieldAttributeFactory()); + attr.setContainer(this.node); + this.attributes.push(attr); + } + this.update({ + attributes: this.attributes, + }); + return this; + } + + setComments(comments: string[]) { + this.comments = comments; + this.update({ + comments: this.comments, + }); + return this; + } + + setName(name: string) { + this.name = name; + this.update({ + name: this.name, + }); + return this; + } + + setType(builder: (type: DataFieldTypeFactory) => DataFieldTypeFactory) { + this.type = builder(new DataFieldTypeFactory()).setContainer(this.node); + this.update({ + type: this.type, + }); + return this; + } +} + +export class DataFieldTypeFactory extends AstFactory { + array?: boolean; + optional?: boolean; + reference?: Reference; + type?: BuiltinType; + unsupported?: UnsupportedFieldTypeFactory; + + constructor() { + super({ type: DataFieldType }); + } + + setArray(array: boolean) { + this.array = array; + this.update({ + array: this.array, + }); + return this; + } + + setOptional(optional: boolean) { + this.optional = optional; + this.update({ + optional: this.optional, + }); + return this; + } + + setReference(reference: TypeDeclaration) { + this.reference = { + $refText: reference.name, + ref: reference, + }; + this.update({ + reference: this.reference, + }); + return this; + } + + setType(type: BuiltinType) { + this.type = type; + this.update({ + type: this.type, + }); + return this; + } + + setUnsupported(builder: (a: UnsupportedFieldTypeFactory) => UnsupportedFieldTypeFactory) { + this.unsupported = builder(new UnsupportedFieldTypeFactory()).setContainer(this.node); + this.update({ + unsupported: this.unsupported, + }); + return this; + } +} + +export class UnsupportedFieldTypeFactory extends AstFactory { + value?: AstFactory; + constructor() { + super({ type: UnsupportedFieldType }); + } + setValue(builder: (value: ExpressionBuilder) => AstFactory) { + this.value = builder(ExpressionBuilder()); + this.update({ + value: this.value!, + }); + return this; + } +} + +export class ModelFactory extends AstFactory { + declarations: AstFactory[] = []; + imports: ModelImportFactory[] = []; + constructor() { + super({ type: Model, node: { declarations: [], imports: [] } }); + } + addImport(builder: (b: ModelImportFactory) => ModelImportFactory) { + this.imports.push(builder(new ModelImportFactory()).setContainer(this.node)); + this.update({ + imports: this.imports, + }); + return this; + } + addDeclaration(builder: (b: DeclarationBuilder) => AstFactory) { + this.declarations.push(builder(DeclarationBuilder()).setContainer(this.node)); + this.update({ + declarations: this.declarations, + }); + return this; + } +} + +export class ModelImportFactory extends AstFactory { + path?: string | undefined; + + constructor() { + super({ type: ModelImport }); + } + + setPath(path: string) { + this.path = path; + this.update({ + path: this.path, + }); + return this; + } +} + +export class EnumFactory extends AstFactory { + name?: string; + comments: string[] = []; + fields: EnumFieldFactory[] = []; + attributes: DataModelAttributeFactory[] = []; + + constructor() { + super({ type: Enum, node: { comments: [], fields: [], attributes: [] } }); + } + + addField(builder: (b: EnumFieldFactory) => EnumFieldFactory) { + this.fields.push(builder(new EnumFieldFactory()).setContainer(this.node)); + this.update({ + fields: this.fields, + }); + return this; + } + + addAttribute(builder: (b: DataModelAttributeFactory) => DataModelAttributeFactory) { + this.attributes.push(builder(new DataModelAttributeFactory()).setContainer(this.node)); + this.update({ + attributes: this.attributes, + }); + return this; + } + + setName(name: string) { + this.name = name; + this.update({ + name: this.name, + }); + return this; + } +} + +export class EnumFieldFactory extends AstFactory { + name?: RegularIDWithTypeNames; + comments: string[] = []; + attributes: DataFieldAttributeFactory[] = []; + + constructor() { + super({ type: EnumField, node: { comments: [], attributes: [] } }); + } + + setName(name: RegularIDWithTypeNames) { + this.name = name; + this.update({ + name: this.name, + }); + return this; + } + + addAttribute(builder: (b: DataFieldAttributeFactory) => DataFieldAttributeFactory) { + this.attributes.push(builder(new DataFieldAttributeFactory()).setContainer(this.node)); + this.update({ + attributes: this.attributes, + }); + return this; + } + + addComment(comment: string) { + this.comments.push(comment); + this.update({ + comments: this.comments, + }); + return this; + } +} diff --git a/packages/language/src/factory/expression.ts b/packages/language/src/factory/expression.ts new file mode 100644 index 000000000..19fe16af1 --- /dev/null +++ b/packages/language/src/factory/expression.ts @@ -0,0 +1,308 @@ +import type { Reference } from 'langium'; +import { AstFactory } from './ast-factory'; +import { + Argument, + ArrayExpr, + BinaryExpr, + FieldInitializer, + FunctionDecl, + InvocationExpr, + MemberAccessExpr, + MemberAccessTarget, + ObjectExpr, + ReferenceArg, + ReferenceExpr, + ReferenceTarget, + UnaryExpr, + type Expression, + type RegularID, +} from '../ast'; +import { + BooleanLiteralFactory, + NullExprFactory, + NumberLiteralFactory, + StringLiteralFactory, + ThisExprFactory, +} from './primitives'; + +export const ExpressionBuilder = () => + ({ + get ArrayExpr() { + return new ArrayExprFactory(); + }, + get BinaryExpr() { + return new BinaryExprFactory(); + }, + get BooleanLiteral() { + return new BooleanLiteralFactory(); + }, + get InvocationExpr() { + return new InvocationExprFactory(); + }, + get MemberAccessExpr() { + return new MemberAccessExprFactory(); + }, + get NullExpr() { + return new NullExprFactory(); + }, + get NumberLiteral() { + return new NumberLiteralFactory(); + }, + get ObjectExpr() { + return new ObjectExprFactory(); + }, + get ReferenceExpr() { + return new ReferenceExprFactory(); + }, + get StringLiteral() { + return new StringLiteralFactory(); + }, + get ThisExpr() { + return new ThisExprFactory(); + }, + get UnaryExpr() { + return new UnaryExprFactory(); + }, + }) satisfies ExpressionBuilderType; +type ExpressionBuilderType = { + [K in T['$type']]: AstFactory>; +}; + +type ExpressionFactoryMap = ReturnType; + +export type ExpressionBuilder = Pick< + ExpressionFactoryMap, + Extract +>; + +export class UnaryExprFactory extends AstFactory { + operand?: AstFactory; + + constructor() { + super({ type: UnaryExpr, node: { operator: '!' } }); + } + + setOperand(builder: (a: ExpressionBuilder) => AstFactory) { + this.operand = builder(ExpressionBuilder()); + this.update({ + operand: this.operand, + }); + return this; + } +} + +export class ReferenceExprFactory extends AstFactory { + target?: Reference; + args: ReferenceArgFactory[] = []; + + constructor() { + super({ type: ReferenceExpr, node: { args: [] } }); + } + + setTarget(target: ReferenceTarget) { + this.target = { + $refText: target.name, + ref: target, + }; + this.update({ + target: this.target, + }); + return this; + } + + addArg(builder: (a: ExpressionBuilder) => AstFactory, name?: string) { + const arg = new ReferenceArgFactory().setValue(builder); + if (name) { + arg.setName(name); + } + this.args.push(arg); + this.update({ + args: this.args, + }); + return this; + } +} + +export class ReferenceArgFactory extends AstFactory { + name?: string; + value?: AstFactory; + + constructor() { + super({ type: ReferenceArg }); + } + + setName(name: string) { + this.name = name; + this.update({ + name: this.name, + }); + return this; + } + + setValue(builder: (a: ExpressionBuilder) => AstFactory) { + this.value = builder(ExpressionBuilder()); + this.update({ + value: this.value, + }); + return this; + } +} + +export class MemberAccessExprFactory extends AstFactory { + member?: Reference; + operand?: AstFactory; + + constructor() { + super({ type: MemberAccessExpr }); + } + + setMember(target: Reference) { + this.member = target; + this.update({ + member: this.member, + }); + return this; + } + + setOperand(builder: (b: ExpressionBuilder) => AstFactory) { + this.operand = builder(ExpressionBuilder()); + this.update({ + operand: this.operand, + }); + return this; + } +} + +export class ObjectExprFactory extends AstFactory { + fields: FieldInitializerFactory[] = []; + + constructor() { + super({ type: ObjectExpr, node: { fields: [] } }); + } + + addField(builder: (b: FieldInitializerFactory) => FieldInitializerFactory) { + this.fields.push(builder(new FieldInitializerFactory())); + this.update({ + fields: this.fields, + }); + return this; + } +} + +export class FieldInitializerFactory extends AstFactory { + name?: RegularID; + value?: AstFactory; + + constructor() { + super({ type: FieldInitializer }); + } + + setName(name: RegularID) { + this.name = name; + this.update({ + name: this.name!, + }); + return this; + } + + setValue(builder: (a: ExpressionBuilder) => AstFactory) { + this.value = builder(ExpressionBuilder()); + this.update({ + value: this.value!, + }); + return this; + } +} + +export class InvocationExprFactory extends AstFactory { + args: ArgumentFactory[] = []; + function?: Reference; + + constructor() { + super({ type: InvocationExpr, node: { args: [] } }); + } + + addArg(builder: (arg: ArgumentFactory) => ArgumentFactory) { + this.args.push(builder(new ArgumentFactory())); + this.update({ + args: this.args, + }); + return this; + } + + setFunction(value: FunctionDecl) { + this.function = { + $refText: value.name, + ref: value, + }; + this.update({ + function: this.function!, + }); + return this; + } +} + +export class ArgumentFactory extends AstFactory { + value?: AstFactory; + + constructor() { + super({ type: Argument }); + } + + setValue(builder: (a: ExpressionBuilder) => AstFactory) { + this.value = builder(ExpressionBuilder()); + this.update({ + value: this.value!, + }); + return this; + } +} + +export class ArrayExprFactory extends AstFactory { + items: AstFactory[] = []; + + constructor() { + super({ type: ArrayExpr, node: { items: [] } }); + } + + addItem(builder: (a: ExpressionBuilder) => AstFactory) { + this.items.push(builder(ExpressionBuilder())); + this.update({ + items: this.items, + }); + return this; + } +} + +export class BinaryExprFactory extends AstFactory { + operator?: BinaryExpr['operator']; + right?: AstFactory; + left?: AstFactory; + // TODO: add support for CollectionPredicateBinding + + constructor() { + super({ type: BinaryExpr }); + } + + setOperator(operator: BinaryExpr['operator']) { + this.operator = operator; + this.update({ + operator: this.operator!, + }); + return this; + } + setRight(builder: (arg: ExpressionBuilder) => AstFactory) { + this.right = builder(ExpressionBuilder()); + this.update({ + right: this.right!, + }); + return this; + } + setLeft(builder: (arg: ExpressionBuilder) => AstFactory) { + this.left = builder(ExpressionBuilder()); + this.update({ + left: this.left!, + }); + return this; + } +} diff --git a/packages/language/src/factory/index.ts b/packages/language/src/factory/index.ts new file mode 100644 index 000000000..1ea2a286b --- /dev/null +++ b/packages/language/src/factory/index.ts @@ -0,0 +1,5 @@ +export * from './ast-factory'; +export * from './primitives'; +export * from './expression'; +export * from './declaration'; +export * from './attribute'; diff --git a/packages/language/src/factory/primitives.ts b/packages/language/src/factory/primitives.ts new file mode 100644 index 000000000..e97310d54 --- /dev/null +++ b/packages/language/src/factory/primitives.ts @@ -0,0 +1,61 @@ +import { AstFactory } from './ast-factory'; +import { BooleanLiteral, NullExpr, NumberLiteral, StringLiteral, ThisExpr } from '../ast'; + +export class ThisExprFactory extends AstFactory { + constructor() { + super({ type: ThisExpr, node: { value: 'this' } }); + } +} + +export class NullExprFactory extends AstFactory { + constructor() { + super({ type: NullExpr, node: { value: 'null' } }); + } +} + +export class NumberLiteralFactory extends AstFactory { + value?: number | string; + + constructor() { + super({ type: NumberLiteral }); + } + + setValue(value: number | string) { + this.value = value; + this.update({ + value: this.value.toString(), + }); + return this; + } +} + +export class StringLiteralFactory extends AstFactory { + value?: string; + + constructor() { + super({ type: StringLiteral }); + } + + setValue(value: string) { + this.value = value; + this.update({ + value: this.value, + }); + return this; + } +} +export class BooleanLiteralFactory extends AstFactory { + value?: boolean; + + constructor() { + super({ type: BooleanLiteral }); + } + + setValue(value: boolean) { + this.value = value; + this.update({ + value: this.value, + }); + return this; + } +} diff --git a/packages/language/src/validators/datamodel-validator.ts b/packages/language/src/validators/datamodel-validator.ts index 6c5d18ffd..d2fcd155d 100644 --- a/packages/language/src/validators/datamodel-validator.ts +++ b/packages/language/src/validators/datamodel-validator.ts @@ -44,13 +44,15 @@ export default class DataModelValidator implements AstValidator { const uniqueFields = allFields.filter((f) => f.attributes.find((attr) => attr.decl.ref?.name === '@unique')); const modelLevelIds = getModelIdFields(dm); const modelUniqueFields = getModelUniqueFields(dm); + const ignore = hasAttribute(dm, '@@ignore'); if ( !dm.isView && idFields.length === 0 && modelLevelIds.length === 0 && uniqueFields.length === 0 && - modelUniqueFields.length === 0 + modelUniqueFields.length === 0 && + !ignore ) { accept( 'error', diff --git a/packages/language/src/zmodel-code-generator.ts b/packages/language/src/zmodel-code-generator.ts index 1e0366ede..5b8373166 100644 --- a/packages/language/src/zmodel-code-generator.ts +++ b/packages/language/src/zmodel-code-generator.ts @@ -28,6 +28,7 @@ import { LiteralExpr, MemberAccessExpr, Model, + ModelImport, NullExpr, NumberLiteral, ObjectExpr, @@ -70,7 +71,7 @@ function gen(name: string) { */ export class ZModelCodeGenerator { private readonly options: ZModelCodeOptions; - + private readonly quote: string; constructor(options?: Partial) { this.options = { binaryExprNumberOfSpaces: options?.binaryExprNumberOfSpaces ?? 1, @@ -78,6 +79,7 @@ export class ZModelCodeGenerator { indent: options?.indent ?? 4, quote: options?.quote ?? 'single', }; + this.quote = this.options.quote === 'double' ? '"' : "'"; } /** @@ -91,9 +93,21 @@ export class ZModelCodeGenerator { return handler.value.call(this, ast); } + private quotedStr(val: string): string { + const trimmedVal = val.replace(new RegExp(`(? this.generate(d)).join('\n\n'); + return `${ast.imports.map((d) => this.generate(d)).join('\n')}${ast.imports.length > 0 ? '\n\n' : ''}${ast.declarations + .sort((a, b) => { + if (a.$type === 'Enum' && b.$type !== 'Enum') return 1; + if (a.$type !== 'Enum' && b.$type === 'Enum') return -1; + return 0; + }) + .map((d) => this.generate(d)) + .join('\n\n')}`; } @gen(DataSource) @@ -103,10 +117,19 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} }`; } + @gen(ModelImport) + private _generateModelImport(ast: ModelImport) { + return `import ${this.quotedStr(ast.path)}`; + } + @gen(Enum) private _generateEnum(ast: Enum) { return `enum ${ast.name} { -${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} +${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') + : '' + } }`; } @@ -159,8 +182,10 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} @gen(DataModel) private _generateDataModel(ast: DataModel) { - return `${ast.isView ? 'view' : 'model'} ${ast.name}${ - ast.mixins.length > 0 ? ' mixes ' + ast.mixins.map((x) => x.$refText).join(', ') : '' + const comments = `${ast.comments.join('\n')}\n`; + + return `${ast.comments.length > 0 ? comments : ''}${ast.isView ? 'view' : 'model'} ${ast.name}${ + ast.mixins.length > 0 ? ' with ' + ast.mixins.map((x) => x.$refText).join(', ') : '' } { ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ ast.attributes.length > 0 @@ -172,9 +197,20 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(DataField) private _generateDataField(ast: DataField) { - return `${ast.name} ${this.fieldType(ast.type)}${ + const fieldLine = `${ast.name} ${this.fieldType(ast.type)}${ ast.attributes.length > 0 ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : '' }`; + + if (ast.comments.length === 0) { + return fieldLine; + } + + // Build comment block with proper indentation: + // - First comment: no indent (caller adds it via `this.indent + this.generate(x)`) + // - Subsequent comments: add indent + // - Field line: add indent (since it comes after the comment block) + const commentLines = ast.comments.map((c, i) => (i === 0 ? c : this.indent + c)); + return `${commentLines.join('\n')}\n${this.indent}${fieldLine}`; } private fieldType(type: DataFieldType) { @@ -226,7 +262,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.quotedStr(ast.value as string); } @gen(NumberLiteral) @@ -271,7 +307,7 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(ReferenceArg) private _generateReferenceArg(ast: ReferenceArg) { - return `${ast.name}:${this.generate(ast.value)}`; + return `${ast.name}: ${this.generate(ast.value)}`; } @gen(MemberAccessExpr) diff --git a/packages/language/tsup.config.ts b/packages/language/tsup.config.ts index 0d5d2b6c4..48282a08c 100644 --- a/packages/language/tsup.config.ts +++ b/packages/language/tsup.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ index: 'src/index.ts', ast: 'src/ast.ts', utils: 'src/utils.ts', + factory: 'src/factory/index.ts', }, outDir: 'dist', splitting: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f519e5c3..f6e6a972e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,6 +201,9 @@ importers: '@zenstackhq/orm': specifier: workspace:* version: link:../orm + '@zenstackhq/schema': + specifier: workspace:* + version: link:../schema '@zenstackhq/sdk': specifier: workspace:* version: link:../sdk @@ -12980,7 +12983,7 @@ snapshots: eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.29.0(jiti@2.6.1)) @@ -13013,7 +13016,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13028,7 +13031,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 From f8b28b6340425d87d258b605a14456f19f256d9a Mon Sep 17 00:00:00 2001 From: Lukas Kahwe Smith Date: Sun, 8 Feb 2026 03:04:41 +0100 Subject: [PATCH 3/8] validate computed field configuration on startup (#653) --- packages/orm/src/client/client-impl.ts | 38 +++++++ packages/testtools/src/client.ts | 5 + .../orm/client-api/computed-fields.test.ts | 98 +++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index fc8f92c7c..acf888f8a 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -75,6 +75,11 @@ export class ClientImpl { ...this.$options.functions, }; + if (!baseClient) { + // validate computed fields configuration once for the root client + this.validateComputedFieldsConfig(); + } + // here we use kysely's props constructor so we can pass a custom query executor if (baseClient) { this.kyselyProps = { @@ -139,6 +144,39 @@ export class ClientImpl { return new ClientImpl(this.schema, this.$options, this, executor); } + /** + * Validates that all computed fields in the schema have corresponding configurations. + */ + private validateComputedFieldsConfig() { + const computedFieldsConfig = + 'computedFields' in this.$options + ? (this.$options.computedFields as Record | undefined) + : undefined; + + for (const [modelName, modelDef] of Object.entries(this.$schema.models)) { + if (modelDef.computedFields) { + for (const fieldName of Object.keys(modelDef.computedFields)) { + const modelConfig = computedFieldsConfig?.[modelName]; + const fieldConfig = modelConfig?.[fieldName]; + // Check if the computed field has a configuration + if (fieldConfig === null || fieldConfig === undefined) { + throw createConfigError( + `Computed field "${fieldName}" in model "${modelName}" does not have a configuration. ` + + `Please provide an implementation in the computedFields option.`, + ); + } + // Check that the configuration is a function + if (typeof fieldConfig !== 'function') { + throw createConfigError( + `Computed field "${fieldName}" in model "${modelName}" has an invalid configuration: ` + + `expected a function but received ${typeof fieldConfig}.`, + ); + } + } + } + } + } + // overload for interactive transaction $transaction( callback: (tx: ClientContract) => Promise, diff --git a/packages/testtools/src/client.ts b/packages/testtools/src/client.ts index 89148c405..69513eeb4 100644 --- a/packages/testtools/src/client.ts +++ b/packages/testtools/src/client.ts @@ -104,6 +104,11 @@ type ExtraTestClientOptions = { globPattern: string; destination: string; }[]; + + /** + * Computed fields configuration for tests. + */ + computedFields?: import('@zenstackhq/orm').ComputedFieldsOptions; }; export type CreateTestClientOptions = Omit, 'dialect'> & diff --git a/tests/e2e/orm/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts index c6470a720..1816854b8 100644 --- a/tests/e2e/orm/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -3,6 +3,94 @@ import { sql } from 'kysely'; import { describe, expect, it } from 'vitest'; describe('Computed fields tests', () => { + it('throws error when computed field configuration is missing', async () => { + await expect( + createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + name String + upperName String @computed +} +`, + { + // missing computedFields configuration + } as any, + ), + ).rejects.toThrow('Computed field "upperName" in model "User" does not have a configuration'); + }); + + it('throws error when computed field is missing from configuration', async () => { + await expect( + createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + name String + upperName String @computed + lowerName String @computed +} +`, + { + computedFields: { + User: { + // only providing one of two computed fields + upperName: (eb: any) => eb.fn('upper', ['name']), + }, + }, + } as any, + ), + ).rejects.toThrow('Computed field "lowerName" in model "User" does not have a configuration'); + }); + + it('throws error when computed field configuration is not a function', async () => { + await expect( + createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + name String + upperName String @computed +} +`, + { + computedFields: { + User: { + // providing a string instead of a function + upperName: 'not a function' as any, + }, + }, + } as any, + ), + ).rejects.toThrow( + 'Computed field "upperName" in model "User" has an invalid configuration: expected a function but received string', + ); + }); + + it('throws error when computed field configuration is a non-function object', async () => { + await expect( + createTestClient( + ` +model User { + id Int @id @default(autoincrement()) + name String + computed1 String @computed +} +`, + { + computedFields: { + User: { + // providing an object instead of a function + computed1: { key: 'value' } as any, + }, + }, + } as any, + ), + ).rejects.toThrow( + 'Computed field "computed1" in model "User" has an invalid configuration: expected a function but received object', + ); + }); + it('works with non-optional fields', async () => { const db = await createTestClient( ` @@ -102,6 +190,11 @@ model User { } `, { + computedFields: { + User: { + upperName: (eb: any) => eb.fn('upper', ['name']), + }, + }, extraSourceFiles: { main: ` import { ZenStackClient } from '@zenstackhq/orm'; @@ -169,6 +262,11 @@ model User { } `, { + computedFields: { + User: { + upperName: (eb: any) => eb.lit(null), + }, + }, extraSourceFiles: { main: ` import { ZenStackClient } from '@zenstackhq/orm'; From f3a24dd5d595b7d947a233bda6d2d9208027e55e Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 8 Feb 2026 11:29:32 +0800 Subject: [PATCH 4/8] chore: rebuild and update test schemas (#671) --- packages/clients/tanstack-query/test/schemas/basic/models.ts | 2 +- samples/sveltekit/src/zenstack/models.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clients/tanstack-query/test/schemas/basic/models.ts b/packages/clients/tanstack-query/test/schemas/basic/models.ts index a4bcedbbd..84371224a 100644 --- a/packages/clients/tanstack-query/test/schemas/basic/models.ts +++ b/packages/clients/tanstack-query/test/schemas/basic/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema-lite"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; export type Category = $ModelResult<$Schema, "Category">; diff --git a/samples/sveltekit/src/zenstack/models.ts b/samples/sveltekit/src/zenstack/models.ts index 3314c7d48..d878eac47 100644 --- a/samples/sveltekit/src/zenstack/models.ts +++ b/samples/sveltekit/src/zenstack/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema-lite"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; /** * User model */ From d9cdd9c8bc12927def230acaa50db81f9db3781c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n=20Benj=C3=A1min?= Date: Tue, 10 Feb 2026 08:57:49 +0100 Subject: [PATCH 5/8] fix(introspection): preserve data validation attributes, enum ordering, and comments during db pull (#672) * fix(introspection): preserve schema integrity during db pull - Retain data validation attributes (e.g., @email) on fields after introspection (#670) - Preserve original declaration order of enums instead of moving them to the end of the schema file (#669) - Preserve triple-slash comments above enum declarations (#669) Fixes #669, fixes #670 * fix: address PR comments * fix(cli): improve db pull for relations and defaults Prevents field name collisions during introspection by refining the naming strategy for self-referencing relations with multiple foreign keys. Extends support for JSON and Bytes default values across MySQL, PostgreSQL, and SQLite providers to ensure consistent schema restoration. Adds test cases for self-referencing models to verify the avoidance of duplicate fields. * fix: address PR comments --- packages/cli/src/actions/db.ts | 19 +- packages/cli/src/actions/pull/index.ts | 122 +++++++- .../cli/src/actions/pull/provider/mysql.ts | 4 + .../src/actions/pull/provider/postgresql.ts | 10 + .../cli/src/actions/pull/provider/sqlite.ts | 4 + packages/cli/src/actions/pull/utils.ts | 4 + packages/cli/src/index.ts | 2 +- packages/cli/test/db/pull.test.ts | 265 +++++++++++++++++- .../language/src/zmodel-code-generator.ts | 21 +- 9 files changed, 424 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/actions/db.ts b/packages/cli/src/actions/db.ts index b868566a5..f6fda3844 100644 --- a/packages/cli/src/actions/db.ts +++ b/packages/cli/src/actions/db.ts @@ -12,9 +12,9 @@ import { loadSchemaDocument, requireDataSourceUrl, } from './action-utils'; -import { syncEnums, syncRelation, syncTable, type Relation } from './pull'; +import { consolidateEnums, syncEnums, syncRelation, syncTable, type Relation } from './pull'; import { providers as pullProviders } from './pull/provider'; -import { getDatasource, getDbName, getRelationFieldsKey, getRelationFkName } from './pull/utils'; +import { getDatasource, getDbName, getRelationFieldsKey, getRelationFkName, isDatabaseManagedAttribute } from './pull/utils'; import type { DataSourceProviderType } from '@zenstackhq/schema'; import { CliError } from '../cli-error'; @@ -173,6 +173,10 @@ async function runPull(options: PullOptions) { }); } + // Consolidate per-column enums (e.g., MySQL's synthetic UserStatus/GroupStatus) + // back to shared enums from the original schema (e.g., Status) + consolidateEnums({ newModel, oldModel: model }); + console.log(colors.blue('Schema synced')); const baseDir = path.dirname(path.resolve(schemaFile)); @@ -457,12 +461,13 @@ async function runPull(options: PullOptions) { } return; } + // Track deleted attributes (in original but not in new) originalField.attributes .filter( (attr) => - !f.attributes.find((d) => d.decl.$refText === attr.decl.$refText) && - !['@map', '@@map', '@default', '@updatedAt'].includes(attr.decl.$refText), + !f.attributes.find((d) => d.decl.$refText === attr.decl.$refText) && + isDatabaseManagedAttribute(attr.decl.$refText), ) .forEach((attr) => { const field = attr.$container; @@ -478,7 +483,7 @@ async function runPull(options: PullOptions) { .filter( (attr) => !originalField.attributes.find((d) => d.decl.$refText === attr.decl.$refText) && - !['@map', '@@map', '@default', '@updatedAt'].includes(attr.decl.$refText), + isDatabaseManagedAttribute(attr.decl.$refText), ) .forEach((attr) => { // attach the new attribute to the original field @@ -619,8 +624,8 @@ async function runPull(options: PullOptions) { } const generator = new ZModelCodeGenerator({ - quote: options.quote, - indent: options.indent, + quote: options.quote ?? 'single', + indent: options.indent ?? 4, }); if (options.output) { diff --git a/packages/cli/src/actions/pull/index.ts b/packages/cli/src/actions/pull/index.ts index b4424746f..998852cb6 100644 --- a/packages/cli/src/actions/pull/index.ts +++ b/packages/cli/src/actions/pull/index.ts @@ -533,13 +533,20 @@ export function syncRelation({ sourceModel.fields.splice(firstSourceFieldId, 0, sourceFieldFactory.node); // Insert the relation field before the first FK scalar field const oppositeFieldPrefix = /[0-9]/g.test(targetModel.name.charAt(0)) ? '_' : ''; - const { name: oppositeFieldName } = resolveNameCasing( + let { name: oppositeFieldName } = resolveNameCasing( options.fieldCasing, similarRelations > 0 ? `${oppositeFieldPrefix}${lowerCaseFirst(sourceModel.name)}_${firstColumn}` : `${lowerCaseFirst(resolveNameCasing(options.fieldCasing, sourceModel.name).name)}${relation.references.type === 'many'? 's' : ''}`, ); + if (targetModel.fields.find((f) => f.name === oppositeFieldName)) { + ({ name: oppositeFieldName } = resolveNameCasing( + options.fieldCasing, + `${lowerCaseFirst(sourceModel.name)}_${firstColumn}To${relation.references.table}_${relation.references.columns[0]}`, + )); + } + const targetFieldFactory = new DataFieldFactory() .setContainer(targetModel) .setName(oppositeFieldName) @@ -556,3 +563,116 @@ export function syncRelation({ targetModel.fields.push(targetFieldFactory.node); } + +/** + * Consolidates per-column enums back to shared enums when possible. + * + * MySQL doesn't have named enum types — each column gets a synthetic enum + * (e.g., `UserStatus`, `GroupStatus`). When the original schema used a shared + * enum (e.g., `Status`) across multiple fields, this function detects the + * mapping via field references and consolidates the synthetic enums back into + * the original shared enum so the merge phase can match them correctly. + */ +export function consolidateEnums({ + newModel, + oldModel, +}: { + newModel: Model; + oldModel: Model; +}) { + const newEnums = newModel.declarations.filter((d) => isEnum(d)) as Enum[]; + const newDataModels = newModel.declarations.filter((d) => d.$type === 'DataModel') as DataModel[]; + const oldDataModels = oldModel.declarations.filter((d) => d.$type === 'DataModel') as DataModel[]; + + // For each new enum, find which old enum it corresponds to (via field references) + const enumMapping = new Map(); // newEnum -> oldEnum + + for (const newEnum of newEnums) { + for (const newDM of newDataModels) { + for (const field of newDM.fields) { + if (field.$type !== 'DataField' || field.type.reference?.ref !== newEnum) continue; + + // Find matching model in old model by db name + const oldDM = oldDataModels.find((d) => getDbName(d) === getDbName(newDM)); + if (!oldDM) continue; + + // Find matching field in old model by db name + const oldField = oldDM.fields.find((f) => getDbName(f) === getDbName(field)); + if (!oldField || oldField.$type !== 'DataField' || !oldField.type.reference?.ref) continue; + + const oldEnum = oldField.type.reference.ref; + if (!isEnum(oldEnum)) continue; + + enumMapping.set(newEnum, oldEnum as Enum); + break; + } + if (enumMapping.has(newEnum)) break; + } + } + + // Group by old enum: oldEnum -> [newEnum1, newEnum2, ...] + const reverseMapping = new Map(); + for (const [newEnum, oldEnum] of enumMapping) { + if (!reverseMapping.has(oldEnum)) { + reverseMapping.set(oldEnum, []); + } + reverseMapping.get(oldEnum)!.push(newEnum); + } + + // Consolidate: when new enums map to the same old enum with matching values + for (const [oldEnum, newEnumsGroup] of reverseMapping) { + const keepEnum = newEnumsGroup[0]!; + + // Skip if already correct (single enum with matching name) + if (newEnumsGroup.length === 1 && keepEnum.name === oldEnum.name) continue; + + // Check that all new enums have the same values as the old enum + const oldValues = new Set(oldEnum.fields.map((f) => getDbName(f))); + const allMatch = newEnumsGroup.every((ne) => { + const newValues = new Set(ne.fields.map((f) => getDbName(f))); + return oldValues.size === newValues.size && [...oldValues].every((v) => newValues.has(v)); + }); + + if (!allMatch) continue; + + // Rename the kept enum to match the old shared name + keepEnum.name = oldEnum.name; + + // Replace keepEnum's attributes with those from the old enum so that + // any synthetic @@map added by syncEnums is removed and getDbName(keepEnum) + // reflects the consolidated name rather than the stale per-column name. + // Shallow-copy and re-parent so AST $container pointers reference keepEnum. + keepEnum.attributes = oldEnum.attributes.map((attr) => { + const copy = { ...attr, $container: keepEnum }; + return copy; + }); + + // Remove duplicate enums from newModel + for (let i = 1; i < newEnumsGroup.length; i++) { + const idx = newModel.declarations.indexOf(newEnumsGroup[i]!); + if (idx >= 0) { + newModel.declarations.splice(idx, 1); + } + } + + // Update all field references in newModel to point to the kept enum + for (const newDM of newDataModels) { + for (const field of newDM.fields) { + if (field.$type !== 'DataField') continue; + const ref = field.type.reference?.ref; + if (ref && newEnumsGroup.includes(ref as Enum)) { + (field.type as any).reference = { + ref: keepEnum, + $refText: keepEnum.name, + }; + } + } + } + + console.log( + colors.gray( + `Consolidated enum${newEnumsGroup.length > 1 ? 's' : ''} ${newEnumsGroup.map((e) => e.name).join(', ')} → ${oldEnum.name}`, + ), + ); + } +} diff --git a/packages/cli/src/actions/pull/provider/mysql.ts b/packages/cli/src/actions/pull/provider/mysql.ts index 895a9cb53..1c8124435 100644 --- a/packages/cli/src/actions/pull/provider/mysql.ts +++ b/packages/cli/src/actions/pull/provider/mysql.ts @@ -266,6 +266,10 @@ export const mysql: IntrospectionProvider = { return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('uuid', services)); } return (ab) => ab.StringLiteral.setValue(val); + case 'Json': + return (ab) => ab.StringLiteral.setValue(val); + case 'Bytes': + return (ab) => ab.StringLiteral.setValue(val); } // Handle function calls (e.g., uuid(), now()) diff --git a/packages/cli/src/actions/pull/provider/postgresql.ts b/packages/cli/src/actions/pull/provider/postgresql.ts index bf54e5658..6bfc9d231 100644 --- a/packages/cli/src/actions/pull/provider/postgresql.ts +++ b/packages/cli/src/actions/pull/provider/postgresql.ts @@ -284,6 +284,16 @@ export const postgresql: IntrospectionProvider = { return (ab) => ab.StringLiteral.setValue(val.slice(1, -1).replace(/''/g, "'")); } return (ab) => ab.StringLiteral.setValue(val); + case 'Json': + if (val.includes('::')) { + return typeCastingConvert({defaultValue,enums,val,services}); + } + return (ab) => ab.StringLiteral.setValue(val); + case 'Bytes': + if (val.includes('::')) { + return typeCastingConvert({defaultValue,enums,val,services}); + } + return (ab) => ab.StringLiteral.setValue(val); } if (val.includes('(') && val.includes(')')) { diff --git a/packages/cli/src/actions/pull/provider/sqlite.ts b/packages/cli/src/actions/pull/provider/sqlite.ts index f58ad0b58..c4b06f367 100644 --- a/packages/cli/src/actions/pull/provider/sqlite.ts +++ b/packages/cli/src/actions/pull/provider/sqlite.ts @@ -394,6 +394,10 @@ export const sqlite: IntrospectionProvider = { return (ab) => ab.StringLiteral.setValue(strippedName); } return (ab) => ab.StringLiteral.setValue(val); + case 'Json': + return (ab) => ab.StringLiteral.setValue(val); + case 'Bytes': + return (ab) => ab.StringLiteral.setValue(val); } console.warn(`Unsupported default value type: "${defaultValue}" for field type "${fieldType}". Skipping default value.`); diff --git a/packages/cli/src/actions/pull/utils.ts b/packages/cli/src/actions/pull/utils.ts index e0abcfdfd..9ec056bc4 100644 --- a/packages/cli/src/actions/pull/utils.ts +++ b/packages/cli/src/actions/pull/utils.ts @@ -28,6 +28,10 @@ export function getAttribute(model: Model, attrName: string) { | undefined; } +export function isDatabaseManagedAttribute(name: string) { + return ['@relation', '@id', '@unique'].includes(name) || name.startsWith('@db.'); +} + export function getDatasource(model: Model) { const datasource = model.declarations.find((d) => d.$type === 'DataSource'); if (!datasource) { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 6e1daafec..bc52a9803 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -170,7 +170,7 @@ function createProgram() { .addOption( new Option('--quote ', 'set the quote style of generated schema files').default('single'), ) - .addOption(new Option('--indent ', 'set the indentation of the generated schema files').default(4).argParser(parseInt)) + .addOption(new Option('--indent ', 'set the indentation of the generated schema files').default(4)) .action((options) => dbAction('pull', options)); dbCommand diff --git a/packages/cli/test/db/pull.test.ts b/packages/cli/test/db/pull.test.ts index 487f6a446..2750a2228 100644 --- a/packages/cli/test/db/pull.test.ts +++ b/packages/cli/test/db/pull.test.ts @@ -102,6 +102,56 @@ model Tag { expect(restoredSchema).toEqual(schema); }); + it('should restore self-referencing model with multiple FK columns without duplicate fields', async () => { + const { workDir, schema } = await createProject( + `model Category { + id Int @id @default(autoincrement()) + categoryParentId Category? @relation('Category_parentIdToCategory', fields: [parentId], references: [id]) + parentId Int? + categoryBuddyId Category? @relation('Category_buddyIdToCategory', fields: [buddyId], references: [id]) + buddyId Int? + categoryMentorId Category? @relation('Category_mentorIdToCategory', fields: [mentorId], references: [id]) + mentorId Int? + categoryParentIdToCategoryId Category[] @relation('Category_parentIdToCategory') + categoryBuddyIdToCategoryId Category[] @relation('Category_buddyIdToCategory') + categoryMentorIdToCategoryId Category[] @relation('Category_mentorIdToCategory') +}`, + ); + runCli('db push', workDir); + + const schemaFile = path.join(workDir, 'zenstack/schema.zmodel'); + + fs.writeFileSync(schemaFile, getDefaultPrelude()); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + + expect(restoredSchema).toEqual(schema); + }); + + it('should preserve self-referencing model with multiple FK columns', async () => { + const { workDir, schema } = await createProject( + `model Category { + id Int @id @default(autoincrement()) + category Category? @relation('Category_parentIdToCategory', fields: [parentId], references: [id]) + parentId Int? + buddy Category? @relation('Category_buddyIdToCategory', fields: [buddyId], references: [id]) + buddyId Int? + mentor Category? @relation('Category_mentorIdToCategory', fields: [mentorId], references: [id]) + mentorId Int? + categories Category[] @relation('Category_parentIdToCategory') + buddys Category[] @relation('Category_buddyIdToCategory') + mentees Category[] @relation('Category_mentorIdToCategory') +}`, + ); + runCli('db push', workDir); + runCli('db pull --indent 4', workDir); + + const restoredSchema = getSchema(workDir); + + expect(restoredSchema).toEqual(schema); + }); + it('should restore one-to-one relation when FK is the single-column primary key', async () => { const { workDir, schema } = await createProject( `model Profile { @@ -224,7 +274,7 @@ model User { id Int @id @default(autoincrement()) email String @unique @map('email_address') name String? @default('Anonymous') - role UsersRole @default(USER) + role Role @default(USER) profile Profile? shared_profile Profile? @relation('shared') posts Post[] @@ -293,7 +343,7 @@ model PostTag { @@map('post_tags') } -enum UsersRole { +enum Role { USER ADMIN MODERATOR @@ -361,6 +411,189 @@ model Post { }); }); + describe('Pull should preserve enum declaration order', () => { + + it('should preserve interleaved enum and model ordering', async () => { + const { workDir, schema } = await createProject( + `enum Role { + USER + ADMIN +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + role Role @default(USER) + status Status @default(ACTIVE) +} + +enum Status { + ACTIVE + INACTIVE + SUSPENDED +}`, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + + // Enum-model-enum ordering should be preserved + expect(getSchema(workDir)).toEqual(schema); + }); + }); + + describe('Pull should consolidate shared enums', () => { + it('should consolidate per-column enums back to the original shared enum', async () => { + const { workDir, schema } = await createProject( + `enum Status { + ACTIVE + INACTIVE + SUSPENDED +} + +model User { + id Int @id @default(autoincrement()) + status Status @default(ACTIVE) +} + +model Group { + id Int @id @default(autoincrement()) + status Status @default(ACTIVE) +}`, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + + // MySQL creates per-column enums (UserStatus, GroupStatus) but + // consolidation should map them back to the original shared Status enum + expect(getSchema(workDir)).toEqual(schema); + }); + + it('should consolidate per-column enums with --always-map without stale @@map', async () => { + // This test targets a bug where consolidateEnums renames keepEnum.name + // to oldEnum.name but leaves the synthetic @@map attribute added by + // syncEnums, so getDbName(keepEnum) still returns the old mapped name + // (e.g., 'UserStatus') instead of the consolidated name ('Status'), + // preventing matching in the downstream delete/add enum logic. + const { workDir } = await createProject( + `enum Status { + ACTIVE + INACTIVE + SUSPENDED +} + +model User { + id Int @id @default(autoincrement()) + status Status @default(ACTIVE) +} + +model Group { + id Int @id @default(autoincrement()) + status Status @default(ACTIVE) +}`, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4 --always-map', workDir); + + const pulledSchema = getSchema(workDir); + + // The consolidated enum should be named Status, not UserStatus/GroupStatus + expect(pulledSchema).toContain('enum Status'); + expect(pulledSchema).not.toContain('enum UserStatus'); + expect(pulledSchema).not.toContain('enum GroupStatus'); + + // There should be no stale @@map referencing the synthetic per-column name + expect(pulledSchema).not.toMatch(/@@map\(['"]UserStatus['"]\)/); + expect(pulledSchema).not.toMatch(/@@map\(['"]GroupStatus['"]\)/); + }); + }); + + describe('Pull should preserve triple-slash comments on enums', () => { + it('should preserve triple-slash comments on enum declarations and fields', async () => { + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + status Status @default(ACTIVE) +} + +/// User account status +/// ACTIVE - user can log in +/// INACTIVE - user is disabled +enum Status { + /// User can log in + ACTIVE + /// User is disabled + INACTIVE + /// User is suspended + SUSPENDED +}`, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + + expect(getSchema(workDir)).toEqual(schema); + }); + }); + + describe('Pull should preserve data validation attributes', () => { + it('should preserve field-level validation attributes after db pull', async () => { + const { workDir, schema } = await createProject( + `model User { + id Int @id @default(autoincrement()) + email String @unique @email + name String @length(min: 2, max: 100) + website String? @url + code String? @regex('^[A-Z]+$') + age Int @gt(0) + score Float @gte(0.0) + rating Decimal @lt(10) + rank BigInt @lte(999) +}`, + ); + runCli('db push', workDir); + + // Pull should preserve all validation attributes + runCli('db pull --indent 4', workDir); + + expect(getSchema(workDir)).toEqual(schema); + }); + + it('should preserve string transformation attributes after db pull', async () => { + const { workDir, schema } = await createProject( + `model Setting { + id Int @id @default(autoincrement()) + key String @trim @lower + value String @trim @upper +}`, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + + expect(getSchema(workDir)).toEqual(schema); + }); + + it('should preserve model-level @@validate attribute after db pull', async () => { + const { workDir, schema } = await createProject( + `model Product { + id Int @id @default(autoincrement()) + minPrice Decimal @default(0.00) + maxPrice Decimal @default(100.00) + + @@validate(minPrice < maxPrice, 'minPrice must be less than maxPrice') +}`, + ); + runCli('db push', workDir); + + runCli('db pull --indent 4', workDir); + + expect(getSchema(workDir)).toEqual(schema); + }); + }); + describe('Pull should update existing field definitions when database changes', () => { it('should update field type when database column type changes', async () => { // Step 1: Create initial schema with String field @@ -534,17 +767,17 @@ model Post { `model User { id Int @id @default(autoincrement()) email String @unique - status UserStatus @default(ACTIVE) - role UserRole @default(USER) + status Status @default(ACTIVE) + role Role @default(USER) } -enum UserStatus { +enum Status { ACTIVE INACTIVE SUSPENDED } -enum UserRole { +enum Role { USER ADMIN MODERATOR @@ -569,7 +802,7 @@ enum UserRole { `model User { id Int @id @default(autoincrement()) email String @unique - status UserStatus @default(ACTIVE) + status Status @default(ACTIVE) posts Post[] metadata Json? @@ -588,7 +821,7 @@ model Post { @@index([authorId]) } -enum UserStatus { +enum Status { ACTIVE INACTIVE SUSPENDED @@ -1097,8 +1330,8 @@ describe('DB pull - SQL specific features', () => { const { workDir, schema } = await createProject( `model User { - id Int @id @default(autoincrement()) - email String @unique + id Int @id @default(autoincrement()) + email String @unique status UserStatus @default(ACTIVE) } @@ -1118,6 +1351,16 @@ enum UserStatus { runCli('db pull --indent 4', workDir); const restoredSchema = getSchema(workDir); - expect(restoredSchema).toEqual(schema); + expect(restoredSchema).toContain(`model User { + id Int @id @default(autoincrement()) + email String @unique + status UserStatus @default(ACTIVE) +}`); + + expect(restoredSchema).toContain(`enum UserStatus { + ACTIVE + INACTIVE + SUSPENDED +}`); }); }); diff --git a/packages/language/src/zmodel-code-generator.ts b/packages/language/src/zmodel-code-generator.ts index 5b8373166..e68ba7735 100644 --- a/packages/language/src/zmodel-code-generator.ts +++ b/packages/language/src/zmodel-code-generator.ts @@ -101,11 +101,6 @@ export class ZModelCodeGenerator { @gen(Model) private _generateModel(ast: Model) { return `${ast.imports.map((d) => this.generate(d)).join('\n')}${ast.imports.length > 0 ? '\n\n' : ''}${ast.declarations - .sort((a, b) => { - if (a.$type === 'Enum' && b.$type !== 'Enum') return 1; - if (a.$type !== 'Enum' && b.$type === 'Enum') return -1; - return 0; - }) .map((d) => this.generate(d)) .join('\n\n')}`; } @@ -124,7 +119,8 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} @gen(Enum) private _generateEnum(ast: Enum) { - return `enum ${ast.name} { + const comments = `${ast.comments.join('\n')}\n`; + return `${ast.comments.length > 0 ? comments : ''}enum ${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') @@ -135,9 +131,20 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ @gen(EnumField) private _generateEnumField(ast: EnumField) { - return `${ast.name}${ + const fieldLine = `${ast.name}${ ast.attributes.length > 0 ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : '' }`; + + if (ast.comments.length === 0) { + return fieldLine; + } + + // Build comment block with proper indentation: + // - First comment: no indent (caller adds it via `this.indent + this.generate(x)`) + // - Subsequent comments: add indent + // - Field line: add indent (since it comes after the comment block) + const commentLines = ast.comments.map((c, i) => (i === 0 ? c : this.indent + c)); + return `${commentLines.join('\n')}\n${this.indent}${fieldLine}`; } @gen(GeneratorDecl) From 9a5f5da40cd82e1534482b9e2cb4bd982b83fc85 Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Tue, 10 Feb 2026 00:13:01 -0800 Subject: [PATCH 6/8] chore: add warning for mismatched versions during `zen generate` (#660) * chore: add warning for mismatched versions during `zen generate` * chore: swap falsy checks for length checks * chore: do not block generation upon error * Add additional new line. * Trigger Build * Trigger Build * fix: addressing PR comments - use `createRequire` instead of dynamic import for better compatibility - use file search to locate nearest package.json file - use longer padding length when formatting warnings * fix: make sure require base dir is absolute path --------- Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- .vscode/launch.json | 2 +- packages/cli/src/actions/action-utils.ts | 47 ++++++++++++++++++++++- packages/cli/src/actions/generate.ts | 45 +++++++++++++++++++++- packages/cli/src/actions/info.ts | 49 +----------------------- 4 files changed, 92 insertions(+), 51 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 09ccbd596..df6fbc3c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "skipFiles": ["/**"], "type": "node", "args": ["generate"], - "cwd": "${workspaceFolder}/samples/blog" + "cwd": "${workspaceFolder}/samples/orm" }, { "name": "Debug with TSX", diff --git a/packages/cli/src/actions/action-utils.ts b/packages/cli/src/actions/action-utils.ts index 2e264593c..c33a81d64 100644 --- a/packages/cli/src/actions/action-utils.ts +++ b/packages/cli/src/actions/action-utils.ts @@ -3,6 +3,7 @@ import { type Model, isDataSource } from '@zenstackhq/language/ast'; import { PrismaSchemaGenerator } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'node:fs'; +import { createRequire } from 'node:module'; import path from 'node:path'; import { CliError } from '../cli-error'; @@ -142,10 +143,10 @@ function findUp( } const target = names.find((name) => fs.existsSync(path.join(cwd, name))); if (multiple === false && target) { - return path.join(cwd, target) as FindUpResult; + return path.resolve(cwd, target) as FindUpResult; } if (target) { - result.push(path.join(cwd, target)); + result.push(path.resolve(cwd, target)); } const up = path.resolve(cwd, '..'); if (up === cwd) { @@ -173,3 +174,45 @@ export function getOutputPath(options: { output?: string }, schemaFile: string) return path.dirname(schemaFile); } } +export async function getZenStackPackages( + searchPath: string, +): Promise> { + const pkgJsonFile = findUp(['package.json'], searchPath, false); + if (!pkgJsonFile) { + return []; + } + + let pkgJson: { + dependencies?: Record; + devDependencies?: Record; + }; + try { + pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf8')); + } catch { + return []; + } + + const packages = Array.from( + new Set( + [...Object.keys(pkgJson.dependencies ?? {}), ...Object.keys(pkgJson.devDependencies ?? {})].filter((p) => + p.startsWith('@zenstackhq/'), + ), + ), + ).sort(); + + const require = createRequire(pkgJsonFile); + + const result = packages.map((pkg) => { + try { + const depPkgJson = require(`${pkg}/package.json`); + if (depPkgJson.private) { + return undefined; + } + return { pkg, version: depPkgJson.version as string }; + } catch { + return { pkg, version: undefined }; + } + }); + + return result.filter((p) => !!p); +} diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index 7ac6db6b2..c014c02ef 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -12,7 +12,8 @@ import { watch } from 'chokidar'; import ora, { type Ora } from 'ora'; import { CliError } from '../cli-error'; import * as corePlugins from '../plugins'; -import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils'; +import { getOutputPath, getSchemaFile, getZenStackPackages, loadSchemaDocument } from './action-utils'; +import semver from 'semver'; type Options = { schema?: string; @@ -27,6 +28,11 @@ type Options = { * CLI action for generating code from schema */ export async function run(options: Options) { + try { + await checkForMismatchedPackages(process.cwd()); + } catch (err) { + console.warn(colors.yellow(`Failed to check for mismatched ZenStack packages: ${err}`)); + } const model = await pureGenerate(options, false); if (options.watch) { @@ -315,3 +321,40 @@ async function loadPluginModule(provider: string, basePath: string) { return undefined; } } + +async function checkForMismatchedPackages(projectPath: string) { + const packages = await getZenStackPackages(projectPath); + if (!packages.length) { + return false; + } + + const versions = new Set(); + for (const { version } of packages) { + if (version) { + versions.add(version); + } + } + + if (versions.size > 1) { + const message = + 'WARNING: Multiple versions of ZenStack packages detected.\n\tThis will probably cause issues and break your types.'; + const slashes = '/'.repeat(73); + const latestVersion = semver.sort(Array.from(versions)).reverse()[0]!; + + console.warn(colors.yellow(`${slashes}\n\n\t${message}\n`)); + for (const { pkg, version } of packages) { + if (!version) continue; + + if (version === latestVersion) { + console.log(`\t${pkg.padEnd(32)}\t${colors.green(version)}`); + } else { + console.log(`\t${pkg.padEnd(32)}\t${colors.yellow(version)}`); + } + } + console.warn(`\n${colors.yellow(slashes)}`); + + return true; + } + + return false; +} diff --git a/packages/cli/src/actions/info.ts b/packages/cli/src/actions/info.ts index bbea51ebb..26e42422f 100644 --- a/packages/cli/src/actions/info.ts +++ b/packages/cli/src/actions/info.ts @@ -1,12 +1,12 @@ import colors from 'colors'; -import path from 'node:path'; +import { getZenStackPackages } from './action-utils'; /** * CLI action for getting information about installed ZenStack packages */ export async function run(projectPath: string) { const packages = await getZenStackPackages(projectPath); - if (!packages) { + if (!packages.length) { console.error('Unable to locate package.json. Are you in a valid project directory?'); return; } @@ -24,48 +24,3 @@ export async function run(projectPath: string) { console.warn(colors.yellow('WARNING: Multiple versions of Zenstack packages detected. This may cause issues.')); } } - -async function getZenStackPackages(projectPath: string): Promise> { - let pkgJson: { - dependencies: Record; - devDependencies: Record; - }; - const resolvedPath = path.resolve(projectPath); - try { - pkgJson = ( - await import(path.join(resolvedPath, 'package.json'), { - with: { type: 'json' }, - }) - ).default; - } catch { - return []; - } - - const packages = Array.from( - new Set( - [...Object.keys(pkgJson.dependencies ?? {}), ...Object.keys(pkgJson.devDependencies ?? {})].filter( - (p) => p.startsWith('@zenstackhq/') || p === 'zenstack', - ), - ), - ).sort(); - - const result = await Promise.all( - packages.map(async (pkg) => { - try { - const depPkgJson = ( - await import(`${pkg}/package.json`, { - with: { type: 'json' }, - }) - ).default; - if (depPkgJson.private) { - return undefined; - } - return { pkg, version: depPkgJson.version as string }; - } catch { - return { pkg, version: undefined }; - } - }), - ); - - return result.filter((p) => !!p); -} From fc703b8f427dc8e22d0b58b01457f6b547e84986 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 11 Feb 2026 11:51:44 +0800 Subject: [PATCH 7/8] chore: clean up repo merging stale files (#2370) --- .devcontainer/.env | 11 - .devcontainer/docker-compose.yml | 37 - .eslintignore | 1 - .eslintrc.json | 19 - .gitattributes | 3 - .github/ISSUE_TEMPLATE/bug_report.md | 5 +- .github/release/.release-manifest.json | 14 - .github/release/release-main-config.json | 63 - .github/workflows/config/codeql-config.yml | 1 - .github/workflows/integration-test.yml | 96 - .github/workflows/regression-test.yml | 96 - .../workflows/security-dependency-review.yml | 33 - .github/workflows/security-ossar.yml | 74 - .github/workflows/security-scorecard.yml | 68 - .npmrc | 3 - CHANGELOG.md | 85 - SECURITY.md | 2 +- jest.config.ts | 35 - packages/LICENSE | 21 - packages/ide/jetbrains/.gitignore | 18 - packages/ide/jetbrains/.idea/.gitignore | 8 - packages/ide/jetbrains/.idea/.name | 1 - packages/ide/jetbrains/.idea/gradle.xml | 17 - packages/ide/jetbrains/.idea/kotlinc.xml | 6 - packages/ide/jetbrains/.idea/misc.xml | 11 - packages/ide/jetbrains/.idea/vcs.xml | 6 - packages/ide/jetbrains/CHANGELOG.md | 138 - packages/ide/jetbrains/build.gradle.kts | 99 - packages/ide/jetbrains/gradle.properties | 6 - .../gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - packages/ide/jetbrains/gradlew | 234 - packages/ide/jetbrains/gradlew.bat | 89 - packages/ide/jetbrains/package.json | 20 - packages/ide/jetbrains/settings.gradle.kts | 8 - .../src/main/kotlin/dev/zenstack/Utils.kt | 28 - .../dev/zenstack/lang/ZModelFileType.kt | 14 - .../kotlin/dev/zenstack/lang/ZModelIcons.kt | 8 - .../dev/zenstack/lang/ZModelLanguage.kt | 7 - .../lsp/ZenStackLspServerDescriptor.kt | 55 - .../lsp/ZenStackLspServerSupportProvider.kt | 20 - .../zenstack/plugin/PluginStateListener.kt | 18 - .../src/main/resources/META-INF/plugin.xml | 67 - .../main/resources/META-INF/pluginIcon.svg | 15 - .../resources/META-INF/pluginIcon_dark.svg | 15 - .../src/main/resources/icons/zmodel.png | Bin 366 -> 0 bytes .../src/main/resources/icons/zmodel_dark.png | Bin 371 -> 0 bytes .../main/textMate/zmodel.tmbundle/info.plist | 16 - packages/language/.eslintrc.json | 4 - packages/language/CHANGELOG.md | 8 - packages/language/LICENSE | 1 - packages/language/README.md | 5 - packages/language/script/generate-plist.ts | 12 - packages/language/syntaxes/zmodel.tmLanguage | 113 - .../language/syntaxes/zmodel.tmLanguage.json | 71 - packages/misc/redwood/CHANGELOG.md | 8 - packages/misc/redwood/LICENSE | 1 - packages/misc/redwood/README.md | 97 - packages/misc/redwood/bin/cli | 3 - packages/misc/redwood/package.json | 57 - packages/misc/redwood/src/cli-passthrough.ts | 49 - packages/misc/redwood/src/commands/setup.ts | 274 - packages/misc/redwood/src/graphql.ts | 60 - packages/misc/redwood/src/index.ts | 11 - packages/misc/redwood/src/setup-package.ts | 11 - packages/misc/redwood/src/utils.ts | 20 - packages/misc/redwood/tsconfig.json | 8 - packages/plugins/openapi/CHANGELOG.md | 8 - packages/plugins/openapi/LICENSE | 1 - packages/plugins/openapi/README.md | 5 - packages/plugins/openapi/jest.config.ts | 1 - packages/plugins/openapi/package.json | 50 - .../plugins/openapi/src/generator-base.ts | 158 - packages/plugins/openapi/src/index.ts | 24 - packages/plugins/openapi/src/meta.ts | 35 - packages/plugins/openapi/src/plugin.zmodel | 9 - .../plugins/openapi/src/rest-generator.ts | 1069 -- packages/plugins/openapi/src/rpc-generator.ts | 963 -- packages/plugins/openapi/src/schema.ts | 44 - .../tests/baseline/rest-3.0.0.baseline.yaml | 3267 ------ .../tests/baseline/rest-3.1.0.baseline.yaml | 3279 ------ .../rest-type-coverage-3.0.0.baseline.yaml | 872 -- .../rest-type-coverage-3.1.0.baseline.yaml | 876 -- .../baseline/rpc-3.0.0-omit.baseline.yaml | 3020 ------ .../tests/baseline/rpc-3.0.0.baseline.yaml | 5789 ----------- .../baseline/rpc-3.1.0-omit.baseline.yaml | 3058 ------ .../tests/baseline/rpc-3.1.0.baseline.yaml | 5853 ----------- .../rpc-type-coverage-3.0.0.baseline.yaml | 3094 ------ .../rpc-type-coverage-3.1.0.baseline.yaml | 3174 ------ .../openapi/tests/openapi-restful.test.ts | 439 - .../plugins/openapi/tests/openapi-rpc.test.ts | 663 -- packages/plugins/openapi/tsconfig.json | 8 - packages/plugins/prisma-types.ts | 31 - packages/plugins/swr/CHANGELOG.md | 8 - packages/plugins/swr/LICENSE | 1 - packages/plugins/swr/README.md | 5 - packages/plugins/swr/jest.config.ts | 1 - packages/plugins/swr/package.json | 61 - packages/plugins/swr/src/generator.ts | 393 - packages/plugins/swr/src/index.ts | 13 - packages/plugins/swr/src/runtime/index.ts | 552 - .../plugins/swr/src/runtime/prisma-types.ts | 1 - .../plugins/swr/tests/react-hooks.test.tsx | 755 -- packages/plugins/swr/tests/swr.test.ts | 129 - packages/plugins/swr/tests/test-model-meta.ts | 57 - packages/plugins/swr/tsconfig.json | 10 - packages/plugins/swr/tsup.config.ts | 11 - packages/plugins/tanstack-query/CHANGELOG.md | 8 - packages/plugins/tanstack-query/LICENSE | 1 - packages/plugins/tanstack-query/README.md | 5 - .../plugins/tanstack-query/jest.config.ts | 1 - packages/plugins/tanstack-query/package.json | 124 - .../tanstack-query/scripts/postbuild.js | 54 - .../plugins/tanstack-query/src/generator.ts | 788 -- packages/plugins/tanstack-query/src/index.ts | 13 - .../tanstack-query/src/runtime-v5/angular.ts | 213 - .../tanstack-query/src/runtime-v5/index.ts | 8 - .../tanstack-query/src/runtime-v5/react.ts | 262 - .../tanstack-query/src/runtime-v5/svelte.ts | 236 - .../tanstack-query/src/runtime-v5/vue.ts | 217 - .../tanstack-query/src/runtime/common.ts | 477 - .../tanstack-query/src/runtime/index.ts | 8 - .../src/runtime/prisma-types.ts | 1 - .../tanstack-query/src/runtime/react.ts | 185 - .../tanstack-query/src/runtime/svelte.ts | 188 - .../plugins/tanstack-query/src/runtime/vue.ts | 215 - .../tanstack-query/tests/plugin.test.ts | 414 - .../tanstack-query/tests/portable.test.ts | 153 - .../tests/react-hooks-v5.test.tsx | 1883 ---- .../tanstack-query/tests/react-hooks.test.tsx | 781 -- .../tanstack-query/tests/test-model-meta.ts | 100 - packages/plugins/tanstack-query/tsconfig.json | 10 - .../plugins/tanstack-query/tsup-v5.config.ts | 11 - .../plugins/tanstack-query/tsup.config.ts | 11 - packages/plugins/trpc/CHANGELOG.md | 8 - packages/plugins/trpc/LICENSE | 1 - packages/plugins/trpc/README.md | 5 - packages/plugins/trpc/jest.config.ts | 1 - packages/plugins/trpc/package.json | 47 - packages/plugins/trpc/res/client/v10/next.ts | 16 - packages/plugins/trpc/res/client/v10/nuxt.ts | 12 - packages/plugins/trpc/res/client/v10/react.ts | 16 - packages/plugins/trpc/res/client/v10/utils.ts | 46 - packages/plugins/trpc/res/client/v11/next.ts | 15 - packages/plugins/trpc/res/client/v11/nuxt.ts | 12 - packages/plugins/trpc/res/client/v11/react.ts | 15 - packages/plugins/trpc/res/client/v11/utils.ts | 1 - .../plugins/trpc/src/client-helper/index.ts | 286 - .../plugins/trpc/src/client-helper/next.ts | 21 - .../plugins/trpc/src/client-helper/nuxt.ts | 57 - .../plugins/trpc/src/client-helper/react.ts | 92 - packages/plugins/trpc/src/generator.ts | 482 - packages/plugins/trpc/src/index.ts | 14 - packages/plugins/trpc/src/project.ts | 16 - packages/plugins/trpc/src/utils.ts | 171 - packages/plugins/trpc/tests/nuxt.test.ts | 43 - .../tests/projects/nuxt-trpc-v10/.gitignore | 26 - .../tests/projects/nuxt-trpc-v10/README.md | 75 - .../trpc/tests/projects/nuxt-trpc-v10/app.vue | 52 - .../projects/nuxt-trpc-v10/nuxt.config.ts | 8 - .../projects/nuxt-trpc-v10/package-lock.json | 8892 ---------------- .../tests/projects/nuxt-trpc-v10/package.json | 28 - .../projects/nuxt-trpc-v10/plugins/client.ts | 23 - .../nuxt-trpc-v10/prisma/schema.prisma | 31 - .../projects/nuxt-trpc-v10/public/favicon.ico | Bin 4286 -> 0 bytes .../projects/nuxt-trpc-v10/schema.zmodel | 55 - .../nuxt-trpc-v10/server/api/trpc/[trpc].ts | 9 - .../tests/projects/nuxt-trpc-v10/server/db.ts | 3 - .../nuxt-trpc-v10/server/trpc/context.ts | 12 - .../generated/client/Post.nuxt.type.ts | 385 - .../generated/client/User.nuxt.type.ts | 385 - .../trpc/routers/generated/client/nuxt.ts | 23 - .../trpc/routers/generated/client/utils.ts | 48 - .../server/trpc/routers/generated/helper.ts | 74 - .../routers/generated/routers/Post.router.ts | 47 - .../routers/generated/routers/User.router.ts | 47 - .../trpc/routers/generated/routers/index.ts | 39 - .../server/trpc/routers/index.ts | 7 - .../nuxt-trpc-v10/server/trpc/trpc.ts | 20 - .../nuxt-trpc-v10/server/tsconfig.json | 3 - .../projects/nuxt-trpc-v10/tsconfig.json | 7 - .../tests/projects/nuxt-trpc-v11/.gitignore | 26 - .../tests/projects/nuxt-trpc-v11/README.md | 75 - .../trpc/tests/projects/nuxt-trpc-v11/app.vue | 50 - .../projects/nuxt-trpc-v11/nuxt.config.ts | 8 - .../projects/nuxt-trpc-v11/package-lock.json | 9061 ----------------- .../tests/projects/nuxt-trpc-v11/package.json | 28 - .../projects/nuxt-trpc-v11/plugins/client.ts | 23 - .../nuxt-trpc-v11/prisma/schema.prisma | 31 - .../projects/nuxt-trpc-v11/public/favicon.ico | Bin 4286 -> 0 bytes .../projects/nuxt-trpc-v11/schema.zmodel | 58 - .../nuxt-trpc-v11/server/api/trpc/[trpc].ts | 9 - .../tests/projects/nuxt-trpc-v11/server/db.ts | 3 - .../nuxt-trpc-v11/server/trpc/context.ts | 12 - .../trpc/routers/generated-router-helper.ts | 2 - .../generated/client/Post.nuxt.type.ts | 385 - .../generated/client/User.nuxt.type.ts | 385 - .../trpc/routers/generated/client/nuxt.ts | 23 - .../trpc/routers/generated/client/utils.ts | 48 - .../server/trpc/routers/generated/helper.ts | 74 - .../routers/generated/routers/Post.router.ts | 49 - .../routers/generated/routers/User.router.ts | 49 - .../trpc/routers/generated/routers/index.ts | 26 - .../server/trpc/routers/index.ts | 6 - .../nuxt-trpc-v11/server/trpc/trpc.ts | 20 - .../nuxt-trpc-v11/server/tsconfig.json | 3 - .../projects/nuxt-trpc-v11/tsconfig.json | 7 - .../tests/projects/t3-trpc-v10/.eslintrc.cjs | 37 - .../tests/projects/t3-trpc-v10/.gitignore | 44 - .../trpc/tests/projects/t3-trpc-v10/README.md | 28 - .../tests/projects/t3-trpc-v10/next.config.js | 22 - .../tests/projects/t3-trpc-v10/package.json | 45 - .../projects/t3-trpc-v10/prisma/schema.prisma | 31 - .../projects/t3-trpc-v10/public/favicon.ico | Bin 15406 -> 0 bytes .../tests/projects/t3-trpc-v10/schema.zmodel | 35 - .../tests/projects/t3-trpc-v10/src/env.js | 40 - .../projects/t3-trpc-v10/src/pages/_app.tsx | 11 - .../t3-trpc-v10/src/pages/api/trpc/[trpc].ts | 19 - .../t3-trpc-v10/src/pages/index.module.css | 177 - .../projects/t3-trpc-v10/src/pages/index.tsx | 39 - .../t3-trpc-v10/src/server/api/root.ts | 16 - .../generated/client/Post.next.type.ts | 383 - .../generated/client/User.next.type.ts | 383 - .../api/routers/generated/client/next.ts | 27 - .../api/routers/generated/client/utils.ts | 48 - .../server/api/routers/generated/helper.ts | 74 - .../routers/generated/routers/Post.router.ts | 47 - .../routers/generated/routers/User.router.ts | 47 - .../api/routers/generated/routers/index.ts | 39 - .../src/server/api/routers/greet.ts | 11 - .../src/server/api/routers/post.ts | 4 - .../t3-trpc-v10/src/server/api/trpc.ts | 95 - .../projects/t3-trpc-v10/src/server/db.ts | 16 - .../t3-trpc-v10/src/styles/globals.css | 16 - .../projects/t3-trpc-v10/src/utils/api.ts | 68 - .../tests/projects/t3-trpc-v10/tsconfig.json | 42 - .../tests/projects/t3-trpc-v11/.eslintrc.cjs | 42 - .../tests/projects/t3-trpc-v11/.gitignore | 46 - .../trpc/tests/projects/t3-trpc-v11/README.md | 29 - .../tests/projects/t3-trpc-v11/next.config.js | 10 - .../projects/t3-trpc-v11/package-lock.json | 4803 --------- .../tests/projects/t3-trpc-v11/package.json | 49 - .../projects/t3-trpc-v11/prisma/schema.prisma | 31 - .../projects/t3-trpc-v11/public/favicon.ico | Bin 15406 -> 0 bytes .../tests/projects/t3-trpc-v11/schema.zmodel | 41 - .../t3-trpc-v11/src/app/_components/post.tsx | 78 - .../src/app/api/trpc/[trpc]/route.ts | 34 - .../t3-trpc-v11/src/app/index.module.css | 177 - .../projects/t3-trpc-v11/src/app/layout.tsx | 24 - .../projects/t3-trpc-v11/src/app/page.tsx | 37 - .../tests/projects/t3-trpc-v11/src/env.js | 42 - .../t3-trpc-v11/src/server/api/root.ts | 21 - .../api/routers/generated-router-helper.ts | 2 - .../generated/client/Post.react.type.ts | 584 -- .../generated/client/User.react.type.ts | 584 -- .../api/routers/generated/client/react.ts | 26 - .../api/routers/generated/client/utils.ts | 48 - .../server/api/routers/generated/helper.ts | 74 - .../routers/generated/routers/Post.router.ts | 49 - .../routers/generated/routers/User.router.ts | 49 - .../api/routers/generated/routers/index.ts | 26 - .../t3-trpc-v11/src/server/api/trpc.ts | 105 - .../projects/t3-trpc-v11/src/server/db.ts | 17 - .../t3-trpc-v11/src/styles/globals.css | 16 - .../t3-trpc-v11/src/trpc/query-client.ts | 25 - .../projects/t3-trpc-v11/src/trpc/react.tsx | 76 - .../projects/t3-trpc-v11/src/trpc/server.ts | 30 - .../tests/projects/t3-trpc-v11/tsconfig.json | 42 - packages/plugins/trpc/tests/t3.test.ts | 43 - packages/plugins/trpc/tests/trpc.test.ts | 483 - packages/plugins/trpc/tsconfig.json | 8 - packages/runtime/CHANGELOG.md | 8 - packages/runtime/LICENSE | 1 - packages/runtime/README.md | 5 - packages/runtime/jest.config.ts | 1 - packages/runtime/package.json | 136 - packages/runtime/res/enhance-edge.d.ts | 1 - packages/runtime/res/enhance-edge.js | 10 - packages/runtime/res/enhance.d.ts | 1 - packages/runtime/res/enhance.js | 10 - packages/runtime/res/model-meta.d.ts | 1 - packages/runtime/res/model-meta.js | 10 - packages/runtime/res/models.d.ts | 1 - packages/runtime/res/models.js | 1 - packages/runtime/res/zod/index.d.ts | 3 - packages/runtime/res/zod/index.js | 5 - packages/runtime/res/zod/input.d.ts | 1 - packages/runtime/res/zod/input.js | 8 - packages/runtime/res/zod/models.d.ts | 1 - packages/runtime/res/zod/models.js | 8 - packages/runtime/res/zod/objects.d.ts | 1 - packages/runtime/res/zod/objects.js | 8 - packages/runtime/src/browser/index.ts | 1 - packages/runtime/src/browser/serialization.ts | 40 - packages/runtime/src/constants.ts | 82 - packages/runtime/src/cross/clone.ts | 25 - packages/runtime/src/cross/index.ts | 9 - .../runtime/src/cross/model-data-visitor.ts | 43 - packages/runtime/src/cross/model-meta.ts | 246 - packages/runtime/src/cross/mutator.ts | 448 - .../runtime/src/cross/nested-read-visitor.ts | 60 - .../runtime/src/cross/nested-write-visitor.ts | 357 - packages/runtime/src/cross/query-analyzer.ts | 110 - packages/runtime/src/cross/types.ts | 28 - packages/runtime/src/cross/utils.ts | 102 - packages/runtime/src/edge.ts | 1 - packages/runtime/src/encryption/index.ts | 67 - packages/runtime/src/encryption/utils.ts | 96 - packages/runtime/src/enhance-edge.d.ts | 2 - packages/runtime/src/enhance.d.ts | 2 - .../enhancements/edge/create-enhancement.ts | 1 - .../src/enhancements/edge/default-auth.ts | 1 - .../runtime/src/enhancements/edge/delegate.ts | 1 - .../src/enhancements/edge/encryption.ts | 1 - .../runtime/src/enhancements/edge/index.ts | 1 - .../src/enhancements/edge/json-processor.ts | 1 - .../runtime/src/enhancements/edge/logger.ts | 1 - .../runtime/src/enhancements/edge/omit.ts | 1 - .../runtime/src/enhancements/edge/password.ts | 1 - .../enhancements/edge/policy/check-utils.ts | 16 - .../src/enhancements/edge/policy/handler.ts | 1 - .../src/enhancements/edge/policy/index.ts | 1 - .../enhancements/edge/policy/policy-utils.ts | 1 - .../runtime/src/enhancements/edge/promise.ts | 1 - .../runtime/src/enhancements/edge/proxy.ts | 1 - .../src/enhancements/edge/query-utils.ts | 1 - .../runtime/src/enhancements/edge/types.ts | 1 - .../runtime/src/enhancements/edge/utils.ts | 1 - .../src/enhancements/edge/where-visitor.ts | 1 - .../enhancements/node/create-enhancement.ts | 170 - .../src/enhancements/node/default-auth.ts | 232 - .../runtime/src/enhancements/node/delegate.ts | 1595 --- .../src/enhancements/node/encryption.ts | 165 - .../runtime/src/enhancements/node/index.ts | 4 - .../src/enhancements/node/json-processor.ts | 92 - .../runtime/src/enhancements/node/logger.ts | 66 - .../runtime/src/enhancements/node/omit.ts | 97 - .../runtime/src/enhancements/node/password.ts | 69 - .../enhancements/node/policy/check-utils.ts | 124 - .../node/policy/constraint-solver.ts | 219 - .../src/enhancements/node/policy/handler.ts | 1837 ---- .../src/enhancements/node/policy/index.ts | 80 - .../node/policy/logic-solver.d.ts | 109 - .../enhancements/node/policy/policy-utils.ts | 1690 --- .../runtime/src/enhancements/node/promise.ts | 99 - .../runtime/src/enhancements/node/proxy.ts | 394 - .../src/enhancements/node/query-utils.ts | 297 - .../runtime/src/enhancements/node/types.ts | 282 - .../runtime/src/enhancements/node/utils.ts | 44 - .../src/enhancements/node/where-visitor.ts | 99 - packages/runtime/src/error.ts | 21 - packages/runtime/src/index.ts | 7 - packages/runtime/src/local-helpers/index.ts | 8 - .../src/local-helpers/is-plain-object.ts | 23 - .../src/local-helpers/lower-case-first.ts | 3 - .../runtime/src/local-helpers/param-case.ts | 18 - .../src/local-helpers/simple-traverse.ts | 61 - packages/runtime/src/local-helpers/sleep.ts | 5 - .../src/local-helpers/tiny-invariant.ts | 17 - .../src/local-helpers/upper-case-first.ts | 3 - .../runtime/src/local-helpers/zod-utils.ts | 22 - packages/runtime/src/package.json | 1 - packages/runtime/src/types.ts | 233 - packages/runtime/src/validation.ts | 46 - packages/runtime/src/version.ts | 9 - packages/runtime/src/zod-utils.ts | 134 - .../runtime/tests/policy/reduction.test.ts | 67 - .../runtime/tests/zod/smart-union.test.ts | 109 - packages/runtime/tsconfig.json | 9 - packages/runtime/tsup-browser.config.ts | 11 - packages/runtime/tsup-cross.config.ts | 11 - packages/schema/.gitignore | 6 - packages/schema/.vscodeignore | 13 - packages/schema/LICENSE | 1 - packages/schema/README-global.md | 1 - packages/schema/README.md | 55 - packages/schema/asset/logo-256-bg.png | Bin 18824 -> 0 bytes packages/schema/asset/logo-dark-256.png | Bin 23301 -> 0 bytes packages/schema/asset/logo-light-256.png | Bin 23566 -> 0 bytes packages/schema/bin/cli | 3 - packages/schema/bin/post-install.js | 31 - packages/schema/build/bundle.js | 44 - packages/schema/build/post-build.js | 29 - packages/schema/jest.config.ts | 1 - packages/schema/language-configuration.json | 32 - packages/schema/src/cli/actions/check.ts | 14 - packages/schema/src/cli/actions/format.ts | 33 - packages/schema/src/cli/actions/generate.ts | 120 - packages/schema/src/cli/actions/index.ts | 6 - packages/schema/src/cli/actions/info.ts | 50 - packages/schema/src/cli/actions/init.ts | 83 - packages/schema/src/cli/actions/repl.ts | 218 - packages/schema/src/cli/cli-error.ts | 4 - packages/schema/src/cli/cli-util.ts | 412 - packages/schema/src/cli/config.ts | 34 - packages/schema/src/cli/index.ts | 180 - packages/schema/src/cli/plugin-runner.ts | 455 - packages/schema/src/constants.ts | 2 - packages/schema/src/extension.ts | 107 - packages/schema/src/global.d.ts | 3 - .../schema/src/language-server/constants.ts | 26 - packages/schema/src/language-server/main.ts | 61 - packages/schema/src/language-server/types.ts | 11 - packages/schema/src/language-server/utils.ts | 37 - .../attribute-application-validator.ts | 411 - .../validator/attribute-validator.ts | 14 - .../validator/datamodel-validator.ts | 446 - .../validator/datasource-validator.ts | 73 - .../validator/enum-validator.ts | 27 - .../validator/expression-validator.ts | 300 - .../validator/function-decl-validator.ts | 13 - .../function-invocation-validator.ts | 307 - .../validator/schema-validator.ts | 62 - .../validator/typedef-validator.ts | 28 - .../src/language-server/validator/utils.ts | 106 - .../validator/zmodel-validator.ts | 102 - .../src/language-server/zmodel-code-action.ts | 171 - .../zmodel-completion-provider.ts | 377 - .../src/language-server/zmodel-definition.ts | 36 - .../zmodel-documentation-provider.ts | 16 - .../src/language-server/zmodel-formatter.ts | 132 - .../src/language-server/zmodel-highlight.ts | 16 - .../src/language-server/zmodel-hover.ts | 16 - .../src/language-server/zmodel-linker.ts | 548 - .../src/language-server/zmodel-module.ts | 144 - .../src/language-server/zmodel-scope.ts | 257 - .../src/language-server/zmodel-semantic.ts | 106 - .../zmodel-workspace-manager.ts | 172 - packages/schema/src/package.json | 1 - .../enhancer/enhance/auth-type-generator.ts | 147 - .../enhance/checker-type-generator.ts | 55 - .../src/plugins/enhancer/enhance/index.ts | 1135 --- .../enhance/model-typedef-generator.ts | 84 - .../src/plugins/enhancer/enhancer-utils.ts | 20 - packages/schema/src/plugins/enhancer/index.ts | 52 - .../src/plugins/enhancer/model-meta/index.ts | 19 - .../enhancer/policy/constraint-transformer.ts | 411 - .../enhancer/policy/expression-writer.ts | 855 -- .../src/plugins/enhancer/policy/index.ts | 8 - .../enhancer/policy/policy-guard-generator.ts | 748 -- .../src/plugins/enhancer/policy/utils.ts | 592 -- packages/schema/src/plugins/plugin-utils.ts | 138 - .../src/plugins/prisma/indent-string.ts | 9 - packages/schema/src/plugins/prisma/index.ts | 158 - .../src/plugins/prisma/prisma-builder.ts | 379 - .../src/plugins/prisma/schema-generator.ts | 1044 -- packages/schema/src/plugins/zod/generator.ts | 824 -- packages/schema/src/plugins/zod/index.ts | 14 - .../schema/src/plugins/zod/transformer.ts | 960 -- packages/schema/src/plugins/zod/types.ts | 31 - .../src/plugins/zod/utils/schema-gen.ts | 261 - packages/schema/src/res/starter.txt | 43 - packages/schema/src/res/stdlib.zmodel | 761 -- packages/schema/src/telemetry.ts | 154 - packages/schema/src/utils/ast-utils.ts | 353 - packages/schema/src/utils/exec-utils.ts | 18 - packages/schema/src/utils/is-ci.ts | 8 - packages/schema/src/utils/is-container.ts | 23 - packages/schema/src/utils/is-docker.ts | 31 - packages/schema/src/utils/is-wsl.ts | 18 - packages/schema/src/utils/machine-id-utils.ts | 74 - packages/schema/src/utils/pkg-utils.ts | 173 - packages/schema/src/utils/version-utils.ts | 13 - .../schema/src/vscode/documentation-cache.ts | 152 - .../src/vscode/release-notes-manager.ts | 77 - .../res/zmodel-preview-release-notes.html | 95 - .../schema/src/vscode/vscode-telemetry.ts | 75 - .../src/vscode/zenstack-auth-provider.ts | 255 - packages/schema/src/vscode/zmodel-preview.ts | 377 - .../tests/generator/expression-writer.test.ts | 1396 --- .../tests/generator/prisma-builder.test.ts | 125 - .../tests/generator/prisma-generator.test.ts | 569 -- .../tests/generator/prisma/format.prisma | 17 - .../prisma/multi-level-inheritance.prisma | 19 - .../tests/generator/zmodel/schema.zmodel | 18 - .../tests/generator/zmodel/user/user.zmodel | 28 - packages/schema/tests/schema/abstract.test.ts | 84 - packages/schema/tests/schema/abstract.zmodel | 33 - .../schema/tests/schema/all-features.zmodel | 190 - packages/schema/tests/schema/cal-com.test.ts | 12 - packages/schema/tests/schema/cal-com.zmodel | 659 -- .../schema/tests/schema/formatter.test.ts | 37 - .../schema/mutil-files/multi-files.test.ts | 12 - .../tests/schema/mutil-files/schema.zmodel | 19 - .../tests/schema/mutil-files/user.zmodel | 10 - packages/schema/tests/schema/parser.test.ts | 577 -- .../schema/tests/schema/sample-todo.test.ts | 12 - packages/schema/tests/schema/stdlib.test.ts | 30 - packages/schema/tests/schema/todo.zmodel | 182 - .../schema/tests/schema/trigger-dev.test.ts | 12 - .../schema/tests/schema/trigger-dev.zmodel | 1039 -- .../validation/attribute-validation.test.ts | 1394 --- .../validation/cyclic-inheritance.test.ts | 39 - .../validation/datamodel-validation.test.ts | 784 -- .../validation/datasource-validation.test.ts | 100 - .../schema/validation/enum-validation.test.ts | 22 - .../validation/schema-validation.test.ts | 76 - .../tests/schema/zmodel-generator.test.ts | 116 - packages/schema/tests/utils.ts | 99 - packages/sdk/CHANGELOG.md | 8 - packages/sdk/LICENSE | 1 - packages/sdk/README.md | 5 - packages/sdk/src/code-gen.ts | 125 - packages/sdk/src/constants.ts | 18 - .../sdk/src/dmmf-helpers/aggregate-helpers.ts | 79 - .../sdk/src/dmmf-helpers/include-helpers.ts | 94 - packages/sdk/src/dmmf-helpers/index.ts | 7 - .../src/dmmf-helpers/missing-types-helper.ts | 16 - .../sdk/src/dmmf-helpers/model-helpers.ts | 36 - .../sdk/src/dmmf-helpers/modelArgs-helpers.ts | 70 - .../sdk/src/dmmf-helpers/select-helpers.ts | 167 - packages/sdk/src/dmmf-helpers/types.ts | 22 - packages/sdk/src/model-meta-generator.ts | 627 -- packages/sdk/src/names.ts | 25 - packages/sdk/src/package.json | 1 - packages/sdk/src/path.ts | 9 - packages/sdk/src/policy.ts | 69 - packages/sdk/src/prisma.ts | 97 - packages/sdk/src/types.ts | 119 - .../src/typescript-expression-transformer.ts | 573 -- packages/sdk/src/utils.ts | 732 -- packages/sdk/src/validation.ts | 46 - packages/sdk/src/zmodel-code-generator.ts | 393 - packages/server/CHANGELOG.md | 8 - packages/server/LICENSE | 1 - packages/server/README.md | 5 - packages/server/jest.config.ts | 1 - packages/server/src/api/base.ts | 67 - packages/server/src/elysia/handler.ts | 82 - packages/server/src/elysia/index.ts | 1 - packages/server/src/express/index.ts | 2 - packages/server/src/express/middleware.ts | 87 - packages/server/src/fastify/index.ts | 2 - packages/server/src/fastify/plugin.ts | 69 - packages/server/src/hono/handler.ts | 62 - packages/server/src/hono/index.ts | 1 - .../server/src/nestjs/api-handler.service.ts | 56 - packages/server/src/nestjs/index.ts | 3 - .../api-handler-options.interface.ts | 18 - .../server/src/nestjs/interfaces/index.ts | 2 - .../zenstack-module-options.interface.ts | 41 - .../server/src/nestjs/zenstack.constants.ts | 4 - packages/server/src/nestjs/zenstack.module.ts | 55 - packages/server/src/next/app-route-handler.ts | 76 - packages/server/src/next/index.ts | 52 - .../server/src/next/pages-route-handler.ts | 51 - packages/server/src/nuxt/handler.ts | 63 - packages/server/src/nuxt/index.ts | 1 - packages/server/src/shared.ts | 49 - packages/server/src/sveltekit/handler.ts | 88 - packages/server/src/sveltekit/index.ts | 2 - packages/server/src/tanstack-start/handler.ts | 83 - packages/server/src/tanstack-start/index.ts | 23 - packages/server/tests/adapter/elysia.test.ts | 202 - packages/server/tests/adapter/express.test.ts | 223 - packages/server/tests/adapter/fastify.test.ts | 239 - packages/server/tests/adapter/hono.test.ts | 193 - packages/server/tests/adapter/nestjs.test.ts | 425 - packages/server/tests/adapter/next.test.ts | 313 - .../server/tests/adapter/sveltekit.test.ts | 190 - .../tests/adapter/tanstack-start.test.ts | 269 - .../server/tests/api/rest-partial.test.ts | 473 - .../server/tests/api/rest-petstore.test.ts | 118 - packages/server/tests/api/rest.test.ts | 3197 ------ packages/server/tests/api/rpc.test.ts | 575 -- packages/server/tests/utils.ts | 33 - packages/testtools/CHANGELOG.md | 8 - packages/testtools/LICENSE | 1 - packages/testtools/README.md | 1 - packages/testtools/src/db.ts | 31 - packages/testtools/src/jest-ext.ts | 158 - packages/testtools/src/model.ts | 71 - samples/next.js/zenstack/models.ts | 2 +- samples/nuxt/zenstack/models.ts | 2 +- script/set-test-env.ts | 1 - script/test-global-setup.ts | 9 - script/test-scaffold.ts | 26 - test-setup.ts | 9 - tests/integration/.eslintrc.json | 13 - tests/integration/.gitignore | 4 - tests/integration/jest.config.ts | 10 - tests/integration/jest.d.ts | 16 - tests/integration/package.json | 38 - tests/integration/test-run/.gitignore | 1 - tests/integration/test-run/package.json | 21 - tests/integration/test-setup.ts | 17 - tests/integration/tests/cli/format.test.ts | 74 - tests/integration/tests/cli/generate.test.ts | 167 - tests/integration/tests/cli/init.test.ts | 151 - tests/integration/tests/cli/plugins.test.ts | 313 - tests/integration/tests/cli/share.ts | 6 - .../e2e/filter-function-coverage.test.ts | 165 - .../tests/e2e/misc-function-coverage.test.ts | 57 - .../tests/e2e/prisma-methods.test.ts | 131 - .../tests/e2e/todo-presets.test.ts | 36 - .../tests/e2e/type-coverage.test.ts | 61 - .../tests/enhancements/json/crud.test.ts | 498 - .../tests/enhancements/json/typing.test.ts | 421 - .../enhancements/json/validation.test.ts | 141 - .../proxy/extension-context.test.ts | 78 - .../typing/enhancement-typing.test.ts | 60 - .../with-delegate/enhanced-client.test.ts | 1655 --- .../with-delegate/omit-interaction.test.ts | 91 - .../password-interaction.test.ts | 50 - .../with-delegate/plugin-interaction.test.ts | 152 - .../with-delegate/policy-interaction.test.ts | 654 -- .../tests/enhancements/with-delegate/utils.ts | 85 - .../with-delegate/validation.test.ts | 26 - .../with-encrypted/with-encrypted.test.ts | 478 - .../enhancements/with-omit/with-omit.test.ts | 159 - .../with-password/with-password.test.ts | 149 - .../enhancements/with-policy/abstract.test.ts | 91 - .../enhancements/with-policy/auth.test.ts | 994 -- .../with-policy/cal-com-sample.test.ts | 11 - .../enhancements/with-policy/checker.test.ts | 763 -- .../with-policy/client-extensions.test.ts | 335 - .../with-policy/connect-disconnect.test.ts | 402 - .../create-many-and-return.test.ts | 110 - .../cross-model-field-comparison.test.ts | 1029 -- .../with-policy/currentModel.test.ts | 185 - .../with-policy/currentOperation.test.ts | 154 - .../with-policy/deep-nested.test.ts | 662 -- .../with-policy/empty-policy.test.ts | 120 - .../with-policy/field-comparison.test.ts | 128 - .../with-policy/field-level-policy.test.ts | 1394 --- .../with-policy/field-validation.test.ts | 995 -- .../with-policy/fluent-api.test.ts | 265 - .../with-policy/multi-field-unique.test.ts | 183 - .../with-policy/multi-file.test.ts | 32 - .../with-policy/multi-id-fields.test.ts | 417 - .../with-policy/nested-to-many.test.ts | 779 -- .../with-policy/nested-to-one.test.ts | 465 - .../enhancements/with-policy/options.test.ts | 55 - .../with-policy/petstore-sample.test.ts | 59 - .../with-policy/post-update.test.ts | 598 -- .../enhancements/with-policy/postgres.test.ts | 527 - .../with-policy/prisma-omit.test.ts | 123 - .../with-policy/prisma-pulse.test.ts | 373 - .../with-policy/query-reduction.test.ts | 147 - .../enhancements/with-policy/refactor.test.ts | 1166 --- .../with-policy/relation-check.test.ts | 703 -- .../relation-many-to-many-filter.test.ts | 296 - .../relation-one-to-many-filter.test.ts | 1026 -- .../relation-one-to-one-filter.test.ts | 1111 -- .../with-policy/self-relation.test.ts | 214 - .../with-policy/todo-sample.test.ts | 512 - .../with-policy/toplevel-operations.test.ts | 274 - .../with-policy/unique-as-id.test.ts | 297 - .../update-many-and-return.test.ts | 140 - .../enhancements/with-policy/view.test.ts | 107 - .../frameworks/nextjs/generation.test.ts | 46 - .../frameworks/nextjs/test-project/.gitignore | 36 - .../frameworks/nextjs/test-project/.npmrc | 1 - .../frameworks/nextjs/test-project/README.md | 34 - .../nextjs/test-project/next.config.js | 7 - .../nextjs/test-project/package.json | 31 - .../nextjs/test-project/pages/_app.tsx | 5 - .../test-project/pages/api/model/[...path].ts | 7 - .../nextjs/test-project/pages/index.tsx | 12 - .../nextjs/test-project/postgres.zmodel | 31 - .../nextjs/test-project/server/db.ts | 12 - .../nextjs/test-project/sqlite.zmodel | 31 - .../nextjs/test-project/tsconfig.json | 20 - .../tests/frameworks/trpc/generation.test.ts | 32 - .../frameworks/trpc/test-project/.gitignore | 36 - .../tests/frameworks/trpc/test-project/.npmrc | 1 - .../frameworks/trpc/test-project/README.md | 34 - .../frameworks/trpc/test-project/lib/trpc.ts | 32 - .../trpc/test-project/next.config.js | 7 - .../frameworks/trpc/test-project/package.json | 36 - .../trpc/test-project/pages/_app.tsx | 8 - .../test-project/pages/api/model/[...path].ts | 7 - .../trpc/test-project/pages/index.tsx | 12 - .../trpc/test-project/server/context.ts | 12 - .../frameworks/trpc/test-project/server/db.ts | 12 - .../trpc/test-project/server/routers/_app.ts | 12 - .../frameworks/trpc/test-project/todo.zmodel | 31 - .../trpc/test-project/tsconfig.json | 23 - .../tests/misc/meta-annotation.test.ts | 59 - .../misc/prisma-client-generator.test.ts | 336 - .../integration/tests/misc/stacktrace.test.ts | 39 - .../integration/tests/plugins/policy.test.ts | 111 - .../integration/tests/plugins/prisma.test.ts | 61 - tests/integration/tests/plugins/zod.test.ts | 1124 -- tests/integration/tests/schema/cal-com.zmodel | 654 -- .../integration/tests/schema/petstore.zmodel | 52 - .../tests/schema/refactor-pg.zmodel | 91 - tests/integration/tests/schema/todo-pg.zmodel | 143 - tests/integration/tests/schema/todo.zmodel | 154 - tests/integration/tsconfig.json | 13 - tests/regression/.eslintrc.json | 13 - tests/regression/jest.config.ts | 10 - tests/regression/jest.d.ts | 16 - tests/regression/test-setup.ts | 17 - tests/regression/tests/issue-1014.test.ts | 51 - tests/regression/tests/issue-1058.test.ts | 53 - tests/regression/tests/issue-1064.test.ts | 291 - tests/regression/tests/issue-1078.test.ts | 55 - tests/regression/tests/issue-1080.test.ts | 132 - tests/regression/tests/issue-1100.test.ts | 69 - tests/regression/tests/issue-1123.test.ts | 47 - tests/regression/tests/issue-1129.test.ts | 87 - tests/regression/tests/issue-1135.test.ts | 85 - tests/regression/tests/issue-1149.test.ts | 112 - tests/regression/tests/issue-1167.test.ts | 20 - tests/regression/tests/issue-1179.test.ts | 27 - tests/regression/tests/issue-1186.test.ts | 51 - tests/regression/tests/issue-1210.test.ts | 92 - tests/regression/tests/issue-1235.test.ts | 35 - tests/regression/tests/issue-1241.test.ts | 87 - tests/regression/tests/issue-1243.test.ts | 55 - tests/regression/tests/issue-1257.test.ts | 53 - tests/regression/tests/issue-1265.test.ts | 27 - tests/regression/tests/issue-1268.test.ts | 32 - tests/regression/tests/issue-1271.test.ts | 191 - tests/regression/tests/issue-1378.test.ts | 47 - tests/regression/tests/issue-1381.test.ts | 61 - tests/regression/tests/issue-1388.test.ts | 26 - tests/regression/tests/issue-1410.test.ts | 146 - tests/regression/tests/issue-1415.test.ts | 22 - tests/regression/tests/issue-1416.test.ts | 37 - tests/regression/tests/issue-1427.test.ts | 42 - tests/regression/tests/issue-1435.test.ts | 119 - tests/regression/tests/issue-1451.test.ts | 57 - tests/regression/tests/issue-1454.test.ts | 117 - tests/regression/tests/issue-1466.test.ts | 236 - tests/regression/tests/issue-1467.test.ts | 51 - tests/regression/tests/issue-1474.test.ts | 27 - tests/regression/tests/issue-1483.test.ts | 68 - tests/regression/tests/issue-1487.test.ts | 71 - tests/regression/tests/issue-1493.test.ts | 66 - tests/regression/tests/issue-1506.test.ts | 39 - tests/regression/tests/issue-1507.test.ts | 27 - tests/regression/tests/issue-1518.test.ts | 31 - tests/regression/tests/issue-1520.test.ts | 70 - tests/regression/tests/issue-1522.test.ts | 92 - tests/regression/tests/issue-1530.test.ts | 36 - tests/regression/tests/issue-1533.test.ts | 66 - tests/regression/tests/issue-1551.test.ts | 26 - tests/regression/tests/issue-1560.test.ts | 41 - tests/regression/tests/issue-1562.test.ts | 45 - tests/regression/tests/issue-1563.test.ts | 30 - tests/regression/tests/issue-1574.test.ts | 108 - tests/regression/tests/issue-1575.test.ts | 29 - tests/regression/tests/issue-1576.test.ts | 63 - tests/regression/tests/issue-1585.test.ts | 30 - tests/regression/tests/issue-1596.test.ts | 34 - tests/regression/tests/issue-1610.test.ts | 56 - tests/regression/tests/issue-1627.test.ts | 50 - tests/regression/tests/issue-1642.test.ts | 40 - tests/regression/tests/issue-1644.test.ts | 23 - tests/regression/tests/issue-1645.test.ts | 203 - tests/regression/tests/issue-1647.test.ts | 69 - tests/regression/tests/issue-1648.test.ts | 43 - tests/regression/tests/issue-1667.test.ts | 86 - tests/regression/tests/issue-1674.test.ts | 87 - tests/regression/tests/issue-1681.test.ts | 29 - tests/regression/tests/issue-1693.test.ts | 20 - tests/regression/tests/issue-1695.test.ts | 21 - tests/regression/tests/issue-1698.test.ts | 74 - tests/regression/tests/issue-1710.test.ts | 53 - tests/regression/tests/issue-1734.test.ts | 107 - tests/regression/tests/issue-1743.test.ts | 34 - tests/regression/tests/issue-1745.test.ts | 98 - tests/regression/tests/issue-1746.test.ts | 149 - tests/regression/tests/issue-1755.test.ts | 61 - tests/regression/tests/issue-1758.test.ts | 29 - tests/regression/tests/issue-1763.test.ts | 47 - tests/regression/tests/issue-177.test.ts | 27 - tests/regression/tests/issue-1770.test.ts | 49 - tests/regression/tests/issue-1786.test.ts | 48 - tests/regression/tests/issue-1835.test.ts | 28 - tests/regression/tests/issue-1843.test.ts | 108 - tests/regression/tests/issue-1849.test.ts | 24 - tests/regression/tests/issue-1857.test.ts | 45 - tests/regression/tests/issue-1859.test.ts | 90 - tests/regression/tests/issue-1870.test.ts | 15 - tests/regression/tests/issue-1894.test.ts | 53 - tests/regression/tests/issue-1930.test.ts | 80 - tests/regression/tests/issue-1955.test.ts | 121 - tests/regression/tests/issue-1964.test.ts | 128 - tests/regression/tests/issue-1978.test.ts | 41 - tests/regression/tests/issue-1984.test.ts | 57 - tests/regression/tests/issue-1991.test.ts | 48 - tests/regression/tests/issue-1992.test.ts | 65 - tests/regression/tests/issue-1993.test.ts | 63 - tests/regression/tests/issue-1994.test.ts | 111 - tests/regression/tests/issue-1997.test.ts | 131 - tests/regression/tests/issue-1998.test.ts | 59 - tests/regression/tests/issue-2000.test.ts | 67 - tests/regression/tests/issue-2007.test.ts | 93 - tests/regression/tests/issue-2014.test.ts | 39 - tests/regression/tests/issue-2019.test.ts | 87 - tests/regression/tests/issue-2025.test.ts | 42 - tests/regression/tests/issue-2028.test.ts | 116 - tests/regression/tests/issue-2038.test.ts | 26 - tests/regression/tests/issue-2039.test.ts | 42 - tests/regression/tests/issue-2065.test.ts | 31 - tests/regression/tests/issue-2106.test.ts | 19 - tests/regression/tests/issue-2117.test.ts | 43 - tests/regression/tests/issue-2168.test.ts | 46 - tests/regression/tests/issue-2175.test.ts | 121 - tests/regression/tests/issue-2226.test.ts | 31 - tests/regression/tests/issue-2246.test.ts | 82 - tests/regression/tests/issue-2283/dev.db | Bin 208896 -> 0 bytes .../tests/issue-2283/regression.test.ts | 683 -- tests/regression/tests/issue-2291.test.ts | 23 - tests/regression/tests/issue-2294.test.ts | 48 - tests/regression/tests/issue-416.test.ts | 17 - tests/regression/tests/issue-646.test.ts | 12 - tests/regression/tests/issue-657.test.ts | 30 - tests/regression/tests/issue-665.test.ts | 38 - tests/regression/tests/issue-674.test.ts | 16 - tests/regression/tests/issue-689.test.ts | 71 - tests/regression/tests/issue-703.test.ts | 26 - tests/regression/tests/issue-714.test.ts | 168 - tests/regression/tests/issue-724.test.ts | 37 - tests/regression/tests/issue-735.test.ts | 21 - tests/regression/tests/issue-744.test.ts | 40 - tests/regression/tests/issue-756.test.ts | 33 - tests/regression/tests/issue-764.test.ts | 49 - tests/regression/tests/issue-765.test.ts | 40 - tests/regression/tests/issue-804.test.ts | 35 - tests/regression/tests/issue-811.test.ts | 71 - tests/regression/tests/issue-814.test.ts | 40 - tests/regression/tests/issue-825.test.ts | 39 - tests/regression/tests/issue-864.test.ts | 185 - tests/regression/tests/issue-886.test.ts | 22 - tests/regression/tests/issue-925.test.ts | 69 - tests/regression/tests/issue-947.test.ts | 19 - tests/regression/tests/issue-961.test.ts | 210 - tests/regression/tests/issue-965.test.ts | 53 - tests/regression/tests/issue-971.test.ts | 23 - tests/regression/tests/issue-992.test.ts | 45 - tests/regression/tests/issue-foo.test.ts | 36 - .../tests/issue-prisma-extension.test.ts | 90 - tests/regression/tests/issues.test.ts | 792 -- tsconfig.base.json | 19 - 838 files changed, 6 insertions(+), 161363 deletions(-) delete mode 100644 .devcontainer/.env delete mode 100644 .devcontainer/docker-compose.yml delete mode 100644 .eslintignore delete mode 100644 .eslintrc.json delete mode 100644 .gitattributes delete mode 100644 .github/release/.release-manifest.json delete mode 100644 .github/release/release-main-config.json delete mode 100644 .github/workflows/integration-test.yml delete mode 100644 .github/workflows/regression-test.yml delete mode 100644 .github/workflows/security-dependency-review.yml delete mode 100644 .github/workflows/security-ossar.yml delete mode 100644 .github/workflows/security-scorecard.yml delete mode 100644 .npmrc delete mode 100644 CHANGELOG.md delete mode 100644 jest.config.ts delete mode 100644 packages/LICENSE delete mode 100644 packages/ide/jetbrains/.gitignore delete mode 100644 packages/ide/jetbrains/.idea/.gitignore delete mode 100644 packages/ide/jetbrains/.idea/.name delete mode 100644 packages/ide/jetbrains/.idea/gradle.xml delete mode 100644 packages/ide/jetbrains/.idea/kotlinc.xml delete mode 100644 packages/ide/jetbrains/.idea/misc.xml delete mode 100644 packages/ide/jetbrains/.idea/vcs.xml delete mode 100644 packages/ide/jetbrains/CHANGELOG.md delete mode 100644 packages/ide/jetbrains/build.gradle.kts delete mode 100644 packages/ide/jetbrains/gradle.properties delete mode 100644 packages/ide/jetbrains/gradle/wrapper/gradle-wrapper.jar delete mode 100644 packages/ide/jetbrains/gradle/wrapper/gradle-wrapper.properties delete mode 100755 packages/ide/jetbrains/gradlew delete mode 100644 packages/ide/jetbrains/gradlew.bat delete mode 100644 packages/ide/jetbrains/package.json delete mode 100644 packages/ide/jetbrains/settings.gradle.kts delete mode 100644 packages/ide/jetbrains/src/main/kotlin/dev/zenstack/Utils.kt delete mode 100644 packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelFileType.kt delete mode 100644 packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelIcons.kt delete mode 100644 packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelLanguage.kt delete mode 100644 packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lsp/ZenStackLspServerDescriptor.kt delete mode 100644 packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lsp/ZenStackLspServerSupportProvider.kt delete mode 100644 packages/ide/jetbrains/src/main/kotlin/dev/zenstack/plugin/PluginStateListener.kt delete mode 100644 packages/ide/jetbrains/src/main/resources/META-INF/plugin.xml delete mode 100644 packages/ide/jetbrains/src/main/resources/META-INF/pluginIcon.svg delete mode 100644 packages/ide/jetbrains/src/main/resources/META-INF/pluginIcon_dark.svg delete mode 100644 packages/ide/jetbrains/src/main/resources/icons/zmodel.png delete mode 100644 packages/ide/jetbrains/src/main/resources/icons/zmodel_dark.png delete mode 100644 packages/ide/jetbrains/src/main/textMate/zmodel.tmbundle/info.plist delete mode 100644 packages/language/.eslintrc.json delete mode 100644 packages/language/CHANGELOG.md delete mode 120000 packages/language/LICENSE delete mode 100644 packages/language/README.md delete mode 100644 packages/language/script/generate-plist.ts delete mode 100644 packages/language/syntaxes/zmodel.tmLanguage delete mode 100644 packages/language/syntaxes/zmodel.tmLanguage.json delete mode 100644 packages/misc/redwood/CHANGELOG.md delete mode 120000 packages/misc/redwood/LICENSE delete mode 100644 packages/misc/redwood/README.md delete mode 100755 packages/misc/redwood/bin/cli delete mode 100644 packages/misc/redwood/package.json delete mode 100644 packages/misc/redwood/src/cli-passthrough.ts delete mode 100644 packages/misc/redwood/src/commands/setup.ts delete mode 100644 packages/misc/redwood/src/graphql.ts delete mode 100644 packages/misc/redwood/src/index.ts delete mode 100644 packages/misc/redwood/src/setup-package.ts delete mode 100644 packages/misc/redwood/src/utils.ts delete mode 100644 packages/misc/redwood/tsconfig.json delete mode 100644 packages/plugins/openapi/CHANGELOG.md delete mode 120000 packages/plugins/openapi/LICENSE delete mode 100644 packages/plugins/openapi/README.md delete mode 120000 packages/plugins/openapi/jest.config.ts delete mode 100644 packages/plugins/openapi/package.json delete mode 100644 packages/plugins/openapi/src/generator-base.ts delete mode 100644 packages/plugins/openapi/src/index.ts delete mode 100644 packages/plugins/openapi/src/meta.ts delete mode 100644 packages/plugins/openapi/src/plugin.zmodel delete mode 100644 packages/plugins/openapi/src/rest-generator.ts delete mode 100644 packages/plugins/openapi/src/rpc-generator.ts delete mode 100644 packages/plugins/openapi/src/schema.ts delete mode 100644 packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rest-type-coverage-3.0.0.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rest-type-coverage-3.1.0.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rpc-3.0.0-omit.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rpc-3.1.0-omit.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml delete mode 100644 packages/plugins/openapi/tests/openapi-restful.test.ts delete mode 100644 packages/plugins/openapi/tests/openapi-rpc.test.ts delete mode 100644 packages/plugins/openapi/tsconfig.json delete mode 100644 packages/plugins/prisma-types.ts delete mode 100644 packages/plugins/swr/CHANGELOG.md delete mode 120000 packages/plugins/swr/LICENSE delete mode 100644 packages/plugins/swr/README.md delete mode 120000 packages/plugins/swr/jest.config.ts delete mode 100644 packages/plugins/swr/package.json delete mode 100644 packages/plugins/swr/src/generator.ts delete mode 100644 packages/plugins/swr/src/index.ts delete mode 100644 packages/plugins/swr/src/runtime/index.ts delete mode 120000 packages/plugins/swr/src/runtime/prisma-types.ts delete mode 100644 packages/plugins/swr/tests/react-hooks.test.tsx delete mode 100644 packages/plugins/swr/tests/swr.test.ts delete mode 100644 packages/plugins/swr/tests/test-model-meta.ts delete mode 100644 packages/plugins/swr/tsconfig.json delete mode 100644 packages/plugins/swr/tsup.config.ts delete mode 100644 packages/plugins/tanstack-query/CHANGELOG.md delete mode 120000 packages/plugins/tanstack-query/LICENSE delete mode 100644 packages/plugins/tanstack-query/README.md delete mode 120000 packages/plugins/tanstack-query/jest.config.ts delete mode 100644 packages/plugins/tanstack-query/package.json delete mode 100755 packages/plugins/tanstack-query/scripts/postbuild.js delete mode 100644 packages/plugins/tanstack-query/src/generator.ts delete mode 100644 packages/plugins/tanstack-query/src/index.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime-v5/angular.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime-v5/index.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime-v5/react.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime-v5/svelte.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime-v5/vue.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime/common.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime/index.ts delete mode 120000 packages/plugins/tanstack-query/src/runtime/prisma-types.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime/react.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime/svelte.ts delete mode 100644 packages/plugins/tanstack-query/src/runtime/vue.ts delete mode 100644 packages/plugins/tanstack-query/tests/plugin.test.ts delete mode 100644 packages/plugins/tanstack-query/tests/portable.test.ts delete mode 100644 packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx delete mode 100644 packages/plugins/tanstack-query/tests/react-hooks.test.tsx delete mode 100644 packages/plugins/tanstack-query/tests/test-model-meta.ts delete mode 100644 packages/plugins/tanstack-query/tsconfig.json delete mode 100644 packages/plugins/tanstack-query/tsup-v5.config.ts delete mode 100644 packages/plugins/tanstack-query/tsup.config.ts delete mode 100644 packages/plugins/trpc/CHANGELOG.md delete mode 120000 packages/plugins/trpc/LICENSE delete mode 100644 packages/plugins/trpc/README.md delete mode 120000 packages/plugins/trpc/jest.config.ts delete mode 100644 packages/plugins/trpc/package.json delete mode 100644 packages/plugins/trpc/res/client/v10/next.ts delete mode 100644 packages/plugins/trpc/res/client/v10/nuxt.ts delete mode 100644 packages/plugins/trpc/res/client/v10/react.ts delete mode 100644 packages/plugins/trpc/res/client/v10/utils.ts delete mode 100644 packages/plugins/trpc/res/client/v11/next.ts delete mode 100644 packages/plugins/trpc/res/client/v11/nuxt.ts delete mode 100644 packages/plugins/trpc/res/client/v11/react.ts delete mode 120000 packages/plugins/trpc/res/client/v11/utils.ts delete mode 100644 packages/plugins/trpc/src/client-helper/index.ts delete mode 100644 packages/plugins/trpc/src/client-helper/next.ts delete mode 100644 packages/plugins/trpc/src/client-helper/nuxt.ts delete mode 100644 packages/plugins/trpc/src/client-helper/react.ts delete mode 100644 packages/plugins/trpc/src/generator.ts delete mode 100644 packages/plugins/trpc/src/index.ts delete mode 100644 packages/plugins/trpc/src/project.ts delete mode 100644 packages/plugins/trpc/src/utils.ts delete mode 100644 packages/plugins/trpc/tests/nuxt.test.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/.gitignore delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/README.md delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/app.vue delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/nuxt.config.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package-lock.json delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/plugins/client.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/prisma/schema.prisma delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/public/favicon.ico delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/schema.zmodel delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/api/trpc/[trpc].ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/db.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/context.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/Post.nuxt.type.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/User.nuxt.type.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/nuxt.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/utils.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/helper.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/Post.router.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/User.router.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/index.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/index.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/trpc.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/tsconfig.json delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v10/tsconfig.json delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/.gitignore delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/README.md delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/app.vue delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/nuxt.config.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package-lock.json delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/plugins/client.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/prisma/schema.prisma delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/public/favicon.ico delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/schema.zmodel delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/api/trpc/[trpc].ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/db.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/context.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated-router-helper.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/Post.nuxt.type.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/User.nuxt.type.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/nuxt.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/utils.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/helper.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/Post.router.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/User.router.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/index.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/index.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/trpc.ts delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/tsconfig.json delete mode 100644 packages/plugins/trpc/tests/projects/nuxt-trpc-v11/tsconfig.json delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/.eslintrc.cjs delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/.gitignore delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/README.md delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/next.config.js delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/prisma/schema.prisma delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/public/favicon.ico delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/schema.zmodel delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/env.js delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/_app.tsx delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/api/trpc/[trpc].ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/index.module.css delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/index.tsx delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/root.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/Post.next.type.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/User.next.type.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/greet.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/post.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/trpc.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/db.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/styles/globals.css delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/src/utils/api.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v10/tsconfig.json delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/.eslintrc.cjs delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/.gitignore delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/README.md delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/next.config.js delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/package-lock.json delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/prisma/schema.prisma delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/public/favicon.ico delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/schema.zmodel delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/_components/post.tsx delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/api/trpc/[trpc]/route.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/index.module.css delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/layout.tsx delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/page.tsx delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/env.js delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/root.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated-router-helper.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/Post.react.type.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/User.react.type.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/react.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/utils.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/helper.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/Post.router.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/User.router.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/index.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/trpc.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/db.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/styles/globals.css delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/query-client.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/react.tsx delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/server.ts delete mode 100644 packages/plugins/trpc/tests/projects/t3-trpc-v11/tsconfig.json delete mode 100644 packages/plugins/trpc/tests/t3.test.ts delete mode 100644 packages/plugins/trpc/tests/trpc.test.ts delete mode 100644 packages/plugins/trpc/tsconfig.json delete mode 100644 packages/runtime/CHANGELOG.md delete mode 120000 packages/runtime/LICENSE delete mode 100644 packages/runtime/README.md delete mode 120000 packages/runtime/jest.config.ts delete mode 100644 packages/runtime/package.json delete mode 100644 packages/runtime/res/enhance-edge.d.ts delete mode 100644 packages/runtime/res/enhance-edge.js delete mode 100644 packages/runtime/res/enhance.d.ts delete mode 100644 packages/runtime/res/enhance.js delete mode 100644 packages/runtime/res/model-meta.d.ts delete mode 100644 packages/runtime/res/model-meta.js delete mode 100644 packages/runtime/res/models.d.ts delete mode 100644 packages/runtime/res/models.js delete mode 100644 packages/runtime/res/zod/index.d.ts delete mode 100644 packages/runtime/res/zod/index.js delete mode 100644 packages/runtime/res/zod/input.d.ts delete mode 100644 packages/runtime/res/zod/input.js delete mode 100644 packages/runtime/res/zod/models.d.ts delete mode 100644 packages/runtime/res/zod/models.js delete mode 100644 packages/runtime/res/zod/objects.d.ts delete mode 100644 packages/runtime/res/zod/objects.js delete mode 100644 packages/runtime/src/browser/index.ts delete mode 100644 packages/runtime/src/browser/serialization.ts delete mode 100644 packages/runtime/src/constants.ts delete mode 100644 packages/runtime/src/cross/clone.ts delete mode 100644 packages/runtime/src/cross/index.ts delete mode 100644 packages/runtime/src/cross/model-data-visitor.ts delete mode 100644 packages/runtime/src/cross/model-meta.ts delete mode 100644 packages/runtime/src/cross/mutator.ts delete mode 100644 packages/runtime/src/cross/nested-read-visitor.ts delete mode 100644 packages/runtime/src/cross/nested-write-visitor.ts delete mode 100644 packages/runtime/src/cross/query-analyzer.ts delete mode 100644 packages/runtime/src/cross/types.ts delete mode 100644 packages/runtime/src/cross/utils.ts delete mode 100644 packages/runtime/src/edge.ts delete mode 100644 packages/runtime/src/encryption/index.ts delete mode 100644 packages/runtime/src/encryption/utils.ts delete mode 100644 packages/runtime/src/enhance-edge.d.ts delete mode 100644 packages/runtime/src/enhance.d.ts delete mode 120000 packages/runtime/src/enhancements/edge/create-enhancement.ts delete mode 120000 packages/runtime/src/enhancements/edge/default-auth.ts delete mode 120000 packages/runtime/src/enhancements/edge/delegate.ts delete mode 120000 packages/runtime/src/enhancements/edge/encryption.ts delete mode 120000 packages/runtime/src/enhancements/edge/index.ts delete mode 120000 packages/runtime/src/enhancements/edge/json-processor.ts delete mode 120000 packages/runtime/src/enhancements/edge/logger.ts delete mode 120000 packages/runtime/src/enhancements/edge/omit.ts delete mode 120000 packages/runtime/src/enhancements/edge/password.ts delete mode 100644 packages/runtime/src/enhancements/edge/policy/check-utils.ts delete mode 120000 packages/runtime/src/enhancements/edge/policy/handler.ts delete mode 120000 packages/runtime/src/enhancements/edge/policy/index.ts delete mode 120000 packages/runtime/src/enhancements/edge/policy/policy-utils.ts delete mode 120000 packages/runtime/src/enhancements/edge/promise.ts delete mode 120000 packages/runtime/src/enhancements/edge/proxy.ts delete mode 120000 packages/runtime/src/enhancements/edge/query-utils.ts delete mode 120000 packages/runtime/src/enhancements/edge/types.ts delete mode 120000 packages/runtime/src/enhancements/edge/utils.ts delete mode 120000 packages/runtime/src/enhancements/edge/where-visitor.ts delete mode 100644 packages/runtime/src/enhancements/node/create-enhancement.ts delete mode 100644 packages/runtime/src/enhancements/node/default-auth.ts delete mode 100644 packages/runtime/src/enhancements/node/delegate.ts delete mode 100644 packages/runtime/src/enhancements/node/encryption.ts delete mode 100644 packages/runtime/src/enhancements/node/index.ts delete mode 100644 packages/runtime/src/enhancements/node/json-processor.ts delete mode 100644 packages/runtime/src/enhancements/node/logger.ts delete mode 100644 packages/runtime/src/enhancements/node/omit.ts delete mode 100644 packages/runtime/src/enhancements/node/password.ts delete mode 100644 packages/runtime/src/enhancements/node/policy/check-utils.ts delete mode 100644 packages/runtime/src/enhancements/node/policy/constraint-solver.ts delete mode 100644 packages/runtime/src/enhancements/node/policy/handler.ts delete mode 100644 packages/runtime/src/enhancements/node/policy/index.ts delete mode 100644 packages/runtime/src/enhancements/node/policy/logic-solver.d.ts delete mode 100644 packages/runtime/src/enhancements/node/policy/policy-utils.ts delete mode 100644 packages/runtime/src/enhancements/node/promise.ts delete mode 100644 packages/runtime/src/enhancements/node/proxy.ts delete mode 100644 packages/runtime/src/enhancements/node/query-utils.ts delete mode 100644 packages/runtime/src/enhancements/node/types.ts delete mode 100644 packages/runtime/src/enhancements/node/utils.ts delete mode 100644 packages/runtime/src/enhancements/node/where-visitor.ts delete mode 100644 packages/runtime/src/error.ts delete mode 100644 packages/runtime/src/index.ts delete mode 100644 packages/runtime/src/local-helpers/index.ts delete mode 100644 packages/runtime/src/local-helpers/is-plain-object.ts delete mode 100644 packages/runtime/src/local-helpers/lower-case-first.ts delete mode 100644 packages/runtime/src/local-helpers/param-case.ts delete mode 100644 packages/runtime/src/local-helpers/simple-traverse.ts delete mode 100644 packages/runtime/src/local-helpers/sleep.ts delete mode 100644 packages/runtime/src/local-helpers/tiny-invariant.ts delete mode 100644 packages/runtime/src/local-helpers/upper-case-first.ts delete mode 100644 packages/runtime/src/local-helpers/zod-utils.ts delete mode 120000 packages/runtime/src/package.json delete mode 100644 packages/runtime/src/types.ts delete mode 100644 packages/runtime/src/validation.ts delete mode 100644 packages/runtime/src/version.ts delete mode 100644 packages/runtime/src/zod-utils.ts delete mode 100644 packages/runtime/tests/policy/reduction.test.ts delete mode 100644 packages/runtime/tests/zod/smart-union.test.ts delete mode 100644 packages/runtime/tsconfig.json delete mode 100644 packages/runtime/tsup-browser.config.ts delete mode 100644 packages/runtime/tsup-cross.config.ts delete mode 100644 packages/schema/.gitignore delete mode 100644 packages/schema/.vscodeignore delete mode 120000 packages/schema/LICENSE delete mode 120000 packages/schema/README-global.md delete mode 100644 packages/schema/README.md delete mode 100644 packages/schema/asset/logo-256-bg.png delete mode 100644 packages/schema/asset/logo-dark-256.png delete mode 100644 packages/schema/asset/logo-light-256.png delete mode 100755 packages/schema/bin/cli delete mode 100644 packages/schema/bin/post-install.js delete mode 100644 packages/schema/build/bundle.js delete mode 100644 packages/schema/build/post-build.js delete mode 120000 packages/schema/jest.config.ts delete mode 100644 packages/schema/language-configuration.json delete mode 100644 packages/schema/src/cli/actions/check.ts delete mode 100644 packages/schema/src/cli/actions/format.ts delete mode 100644 packages/schema/src/cli/actions/generate.ts delete mode 100644 packages/schema/src/cli/actions/index.ts delete mode 100644 packages/schema/src/cli/actions/info.ts delete mode 100644 packages/schema/src/cli/actions/init.ts delete mode 100644 packages/schema/src/cli/actions/repl.ts delete mode 100644 packages/schema/src/cli/cli-error.ts delete mode 100644 packages/schema/src/cli/cli-util.ts delete mode 100644 packages/schema/src/cli/config.ts delete mode 100644 packages/schema/src/cli/index.ts delete mode 100644 packages/schema/src/cli/plugin-runner.ts delete mode 100644 packages/schema/src/constants.ts delete mode 100644 packages/schema/src/extension.ts delete mode 100644 packages/schema/src/global.d.ts delete mode 100644 packages/schema/src/language-server/constants.ts delete mode 100644 packages/schema/src/language-server/main.ts delete mode 100644 packages/schema/src/language-server/types.ts delete mode 100644 packages/schema/src/language-server/utils.ts delete mode 100644 packages/schema/src/language-server/validator/attribute-application-validator.ts delete mode 100644 packages/schema/src/language-server/validator/attribute-validator.ts delete mode 100644 packages/schema/src/language-server/validator/datamodel-validator.ts delete mode 100644 packages/schema/src/language-server/validator/datasource-validator.ts delete mode 100644 packages/schema/src/language-server/validator/enum-validator.ts delete mode 100644 packages/schema/src/language-server/validator/expression-validator.ts delete mode 100644 packages/schema/src/language-server/validator/function-decl-validator.ts delete mode 100644 packages/schema/src/language-server/validator/function-invocation-validator.ts delete mode 100644 packages/schema/src/language-server/validator/schema-validator.ts delete mode 100644 packages/schema/src/language-server/validator/typedef-validator.ts delete mode 100644 packages/schema/src/language-server/validator/utils.ts delete mode 100644 packages/schema/src/language-server/validator/zmodel-validator.ts delete mode 100644 packages/schema/src/language-server/zmodel-code-action.ts delete mode 100644 packages/schema/src/language-server/zmodel-completion-provider.ts delete mode 100644 packages/schema/src/language-server/zmodel-definition.ts delete mode 100644 packages/schema/src/language-server/zmodel-documentation-provider.ts delete mode 100644 packages/schema/src/language-server/zmodel-formatter.ts delete mode 100644 packages/schema/src/language-server/zmodel-highlight.ts delete mode 100644 packages/schema/src/language-server/zmodel-hover.ts delete mode 100644 packages/schema/src/language-server/zmodel-linker.ts delete mode 100644 packages/schema/src/language-server/zmodel-module.ts delete mode 100644 packages/schema/src/language-server/zmodel-scope.ts delete mode 100644 packages/schema/src/language-server/zmodel-semantic.ts delete mode 100644 packages/schema/src/language-server/zmodel-workspace-manager.ts delete mode 120000 packages/schema/src/package.json delete mode 100644 packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts delete mode 100644 packages/schema/src/plugins/enhancer/enhance/checker-type-generator.ts delete mode 100644 packages/schema/src/plugins/enhancer/enhance/index.ts delete mode 100644 packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts delete mode 100644 packages/schema/src/plugins/enhancer/enhancer-utils.ts delete mode 100644 packages/schema/src/plugins/enhancer/index.ts delete mode 100644 packages/schema/src/plugins/enhancer/model-meta/index.ts delete mode 100644 packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts delete mode 100644 packages/schema/src/plugins/enhancer/policy/expression-writer.ts delete mode 100644 packages/schema/src/plugins/enhancer/policy/index.ts delete mode 100644 packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts delete mode 100644 packages/schema/src/plugins/enhancer/policy/utils.ts delete mode 100644 packages/schema/src/plugins/plugin-utils.ts delete mode 100644 packages/schema/src/plugins/prisma/indent-string.ts delete mode 100644 packages/schema/src/plugins/prisma/index.ts delete mode 100644 packages/schema/src/plugins/prisma/prisma-builder.ts delete mode 100644 packages/schema/src/plugins/prisma/schema-generator.ts delete mode 100644 packages/schema/src/plugins/zod/generator.ts delete mode 100644 packages/schema/src/plugins/zod/index.ts delete mode 100644 packages/schema/src/plugins/zod/transformer.ts delete mode 100644 packages/schema/src/plugins/zod/types.ts delete mode 100644 packages/schema/src/plugins/zod/utils/schema-gen.ts delete mode 100644 packages/schema/src/res/starter.txt delete mode 100644 packages/schema/src/res/stdlib.zmodel delete mode 100644 packages/schema/src/telemetry.ts delete mode 100644 packages/schema/src/utils/ast-utils.ts delete mode 100644 packages/schema/src/utils/exec-utils.ts delete mode 100644 packages/schema/src/utils/is-ci.ts delete mode 100644 packages/schema/src/utils/is-container.ts delete mode 100644 packages/schema/src/utils/is-docker.ts delete mode 100644 packages/schema/src/utils/is-wsl.ts delete mode 100644 packages/schema/src/utils/machine-id-utils.ts delete mode 100644 packages/schema/src/utils/pkg-utils.ts delete mode 100644 packages/schema/src/utils/version-utils.ts delete mode 100644 packages/schema/src/vscode/documentation-cache.ts delete mode 100644 packages/schema/src/vscode/release-notes-manager.ts delete mode 100644 packages/schema/src/vscode/res/zmodel-preview-release-notes.html delete mode 100644 packages/schema/src/vscode/vscode-telemetry.ts delete mode 100644 packages/schema/src/vscode/zenstack-auth-provider.ts delete mode 100644 packages/schema/src/vscode/zmodel-preview.ts delete mode 100644 packages/schema/tests/generator/expression-writer.test.ts delete mode 100644 packages/schema/tests/generator/prisma-builder.test.ts delete mode 100644 packages/schema/tests/generator/prisma-generator.test.ts delete mode 100644 packages/schema/tests/generator/prisma/format.prisma delete mode 100644 packages/schema/tests/generator/prisma/multi-level-inheritance.prisma delete mode 100644 packages/schema/tests/generator/zmodel/schema.zmodel delete mode 100644 packages/schema/tests/generator/zmodel/user/user.zmodel delete mode 100644 packages/schema/tests/schema/abstract.test.ts delete mode 100644 packages/schema/tests/schema/abstract.zmodel delete mode 100644 packages/schema/tests/schema/all-features.zmodel delete mode 100644 packages/schema/tests/schema/cal-com.test.ts delete mode 100644 packages/schema/tests/schema/cal-com.zmodel delete mode 100644 packages/schema/tests/schema/formatter.test.ts delete mode 100644 packages/schema/tests/schema/mutil-files/multi-files.test.ts delete mode 100644 packages/schema/tests/schema/mutil-files/schema.zmodel delete mode 100644 packages/schema/tests/schema/mutil-files/user.zmodel delete mode 100644 packages/schema/tests/schema/parser.test.ts delete mode 100644 packages/schema/tests/schema/sample-todo.test.ts delete mode 100644 packages/schema/tests/schema/stdlib.test.ts delete mode 100644 packages/schema/tests/schema/todo.zmodel delete mode 100644 packages/schema/tests/schema/trigger-dev.test.ts delete mode 100644 packages/schema/tests/schema/trigger-dev.zmodel delete mode 100644 packages/schema/tests/schema/validation/attribute-validation.test.ts delete mode 100644 packages/schema/tests/schema/validation/cyclic-inheritance.test.ts delete mode 100644 packages/schema/tests/schema/validation/datamodel-validation.test.ts delete mode 100644 packages/schema/tests/schema/validation/datasource-validation.test.ts delete mode 100644 packages/schema/tests/schema/validation/enum-validation.test.ts delete mode 100644 packages/schema/tests/schema/validation/schema-validation.test.ts delete mode 100644 packages/schema/tests/schema/zmodel-generator.test.ts delete mode 100644 packages/schema/tests/utils.ts delete mode 100644 packages/sdk/CHANGELOG.md delete mode 120000 packages/sdk/LICENSE delete mode 100644 packages/sdk/README.md delete mode 100644 packages/sdk/src/code-gen.ts delete mode 100644 packages/sdk/src/constants.ts delete mode 100644 packages/sdk/src/dmmf-helpers/aggregate-helpers.ts delete mode 100644 packages/sdk/src/dmmf-helpers/include-helpers.ts delete mode 100644 packages/sdk/src/dmmf-helpers/index.ts delete mode 100644 packages/sdk/src/dmmf-helpers/missing-types-helper.ts delete mode 100644 packages/sdk/src/dmmf-helpers/model-helpers.ts delete mode 100644 packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts delete mode 100644 packages/sdk/src/dmmf-helpers/select-helpers.ts delete mode 100644 packages/sdk/src/dmmf-helpers/types.ts delete mode 100644 packages/sdk/src/model-meta-generator.ts delete mode 100644 packages/sdk/src/names.ts delete mode 120000 packages/sdk/src/package.json delete mode 100644 packages/sdk/src/path.ts delete mode 100644 packages/sdk/src/policy.ts delete mode 100644 packages/sdk/src/prisma.ts delete mode 100644 packages/sdk/src/types.ts delete mode 100644 packages/sdk/src/typescript-expression-transformer.ts delete mode 100644 packages/sdk/src/utils.ts delete mode 100644 packages/sdk/src/validation.ts delete mode 100644 packages/sdk/src/zmodel-code-generator.ts delete mode 100644 packages/server/CHANGELOG.md delete mode 120000 packages/server/LICENSE delete mode 100644 packages/server/README.md delete mode 120000 packages/server/jest.config.ts delete mode 100644 packages/server/src/api/base.ts delete mode 100644 packages/server/src/elysia/handler.ts delete mode 100644 packages/server/src/elysia/index.ts delete mode 100644 packages/server/src/express/index.ts delete mode 100644 packages/server/src/express/middleware.ts delete mode 100644 packages/server/src/fastify/index.ts delete mode 100644 packages/server/src/fastify/plugin.ts delete mode 100644 packages/server/src/hono/handler.ts delete mode 100644 packages/server/src/hono/index.ts delete mode 100644 packages/server/src/nestjs/api-handler.service.ts delete mode 100644 packages/server/src/nestjs/index.ts delete mode 100644 packages/server/src/nestjs/interfaces/api-handler-options.interface.ts delete mode 100644 packages/server/src/nestjs/interfaces/index.ts delete mode 100644 packages/server/src/nestjs/interfaces/zenstack-module-options.interface.ts delete mode 100644 packages/server/src/nestjs/zenstack.constants.ts delete mode 100644 packages/server/src/nestjs/zenstack.module.ts delete mode 100644 packages/server/src/next/app-route-handler.ts delete mode 100644 packages/server/src/next/index.ts delete mode 100644 packages/server/src/next/pages-route-handler.ts delete mode 100644 packages/server/src/nuxt/handler.ts delete mode 100644 packages/server/src/nuxt/index.ts delete mode 100644 packages/server/src/shared.ts delete mode 100644 packages/server/src/sveltekit/handler.ts delete mode 100644 packages/server/src/sveltekit/index.ts delete mode 100644 packages/server/src/tanstack-start/handler.ts delete mode 100644 packages/server/src/tanstack-start/index.ts delete mode 100644 packages/server/tests/adapter/elysia.test.ts delete mode 100644 packages/server/tests/adapter/express.test.ts delete mode 100644 packages/server/tests/adapter/fastify.test.ts delete mode 100644 packages/server/tests/adapter/hono.test.ts delete mode 100644 packages/server/tests/adapter/nestjs.test.ts delete mode 100644 packages/server/tests/adapter/next.test.ts delete mode 100644 packages/server/tests/adapter/sveltekit.test.ts delete mode 100644 packages/server/tests/adapter/tanstack-start.test.ts delete mode 100644 packages/server/tests/api/rest-partial.test.ts delete mode 100644 packages/server/tests/api/rest-petstore.test.ts delete mode 100644 packages/server/tests/api/rest.test.ts delete mode 100644 packages/server/tests/api/rpc.test.ts delete mode 100644 packages/server/tests/utils.ts delete mode 100644 packages/testtools/CHANGELOG.md delete mode 120000 packages/testtools/LICENSE delete mode 100644 packages/testtools/README.md delete mode 100644 packages/testtools/src/db.ts delete mode 100644 packages/testtools/src/jest-ext.ts delete mode 100644 packages/testtools/src/model.ts delete mode 100644 script/set-test-env.ts delete mode 100644 script/test-global-setup.ts delete mode 100644 script/test-scaffold.ts delete mode 100644 test-setup.ts delete mode 100644 tests/integration/.eslintrc.json delete mode 100644 tests/integration/.gitignore delete mode 100644 tests/integration/jest.config.ts delete mode 100644 tests/integration/jest.d.ts delete mode 100644 tests/integration/package.json delete mode 100644 tests/integration/test-run/.gitignore delete mode 100644 tests/integration/test-run/package.json delete mode 100644 tests/integration/test-setup.ts delete mode 100644 tests/integration/tests/cli/format.test.ts delete mode 100644 tests/integration/tests/cli/generate.test.ts delete mode 100644 tests/integration/tests/cli/init.test.ts delete mode 100644 tests/integration/tests/cli/plugins.test.ts delete mode 100644 tests/integration/tests/cli/share.ts delete mode 100644 tests/integration/tests/e2e/filter-function-coverage.test.ts delete mode 100644 tests/integration/tests/e2e/misc-function-coverage.test.ts delete mode 100644 tests/integration/tests/e2e/prisma-methods.test.ts delete mode 100644 tests/integration/tests/e2e/todo-presets.test.ts delete mode 100644 tests/integration/tests/e2e/type-coverage.test.ts delete mode 100644 tests/integration/tests/enhancements/json/crud.test.ts delete mode 100644 tests/integration/tests/enhancements/json/typing.test.ts delete mode 100644 tests/integration/tests/enhancements/json/validation.test.ts delete mode 100644 tests/integration/tests/enhancements/proxy/extension-context.test.ts delete mode 100644 tests/integration/tests/enhancements/typing/enhancement-typing.test.ts delete mode 100644 tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts delete mode 100644 tests/integration/tests/enhancements/with-delegate/omit-interaction.test.ts delete mode 100644 tests/integration/tests/enhancements/with-delegate/password-interaction.test.ts delete mode 100644 tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts delete mode 100644 tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts delete mode 100644 tests/integration/tests/enhancements/with-delegate/utils.ts delete mode 100644 tests/integration/tests/enhancements/with-delegate/validation.test.ts delete mode 100644 tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts delete mode 100644 tests/integration/tests/enhancements/with-omit/with-omit.test.ts delete mode 100644 tests/integration/tests/enhancements/with-password/with-password.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/abstract.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/auth.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/cal-com-sample.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/checker.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/client-extensions.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/connect-disconnect.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/create-many-and-return.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/cross-model-field-comparison.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/currentModel.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/currentOperation.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/deep-nested.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/empty-policy.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/field-comparison.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/field-validation.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/fluent-api.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/multi-field-unique.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/multi-file.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/options.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/post-update.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/postgres.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/prisma-omit.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/prisma-pulse.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/query-reduction.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/refactor.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/relation-check.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/relation-many-to-many-filter.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/relation-one-to-many-filter.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/relation-one-to-one-filter.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/self-relation.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/todo-sample.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/unique-as-id.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/update-many-and-return.test.ts delete mode 100644 tests/integration/tests/enhancements/with-policy/view.test.ts delete mode 100644 tests/integration/tests/frameworks/nextjs/generation.test.ts delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/.gitignore delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/.npmrc delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/README.md delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/next.config.js delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/package.json delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/pages/_app.tsx delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/pages/api/model/[...path].ts delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/pages/index.tsx delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/postgres.zmodel delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/server/db.ts delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/sqlite.zmodel delete mode 100644 tests/integration/tests/frameworks/nextjs/test-project/tsconfig.json delete mode 100644 tests/integration/tests/frameworks/trpc/generation.test.ts delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/.gitignore delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/.npmrc delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/README.md delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/lib/trpc.ts delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/next.config.js delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/package.json delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/pages/_app.tsx delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/pages/api/model/[...path].ts delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/pages/index.tsx delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/server/context.ts delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/server/db.ts delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/server/routers/_app.ts delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/todo.zmodel delete mode 100644 tests/integration/tests/frameworks/trpc/test-project/tsconfig.json delete mode 100644 tests/integration/tests/misc/meta-annotation.test.ts delete mode 100644 tests/integration/tests/misc/prisma-client-generator.test.ts delete mode 100644 tests/integration/tests/misc/stacktrace.test.ts delete mode 100644 tests/integration/tests/plugins/policy.test.ts delete mode 100644 tests/integration/tests/plugins/prisma.test.ts delete mode 100644 tests/integration/tests/plugins/zod.test.ts delete mode 100644 tests/integration/tests/schema/cal-com.zmodel delete mode 100644 tests/integration/tests/schema/petstore.zmodel delete mode 100644 tests/integration/tests/schema/refactor-pg.zmodel delete mode 100644 tests/integration/tests/schema/todo-pg.zmodel delete mode 100644 tests/integration/tests/schema/todo.zmodel delete mode 100644 tests/integration/tsconfig.json delete mode 100644 tests/regression/.eslintrc.json delete mode 100644 tests/regression/jest.config.ts delete mode 100644 tests/regression/jest.d.ts delete mode 100644 tests/regression/test-setup.ts delete mode 100644 tests/regression/tests/issue-1014.test.ts delete mode 100644 tests/regression/tests/issue-1058.test.ts delete mode 100644 tests/regression/tests/issue-1064.test.ts delete mode 100644 tests/regression/tests/issue-1078.test.ts delete mode 100644 tests/regression/tests/issue-1080.test.ts delete mode 100644 tests/regression/tests/issue-1100.test.ts delete mode 100644 tests/regression/tests/issue-1123.test.ts delete mode 100644 tests/regression/tests/issue-1129.test.ts delete mode 100644 tests/regression/tests/issue-1135.test.ts delete mode 100644 tests/regression/tests/issue-1149.test.ts delete mode 100644 tests/regression/tests/issue-1167.test.ts delete mode 100644 tests/regression/tests/issue-1179.test.ts delete mode 100644 tests/regression/tests/issue-1186.test.ts delete mode 100644 tests/regression/tests/issue-1210.test.ts delete mode 100644 tests/regression/tests/issue-1235.test.ts delete mode 100644 tests/regression/tests/issue-1241.test.ts delete mode 100644 tests/regression/tests/issue-1243.test.ts delete mode 100644 tests/regression/tests/issue-1257.test.ts delete mode 100644 tests/regression/tests/issue-1265.test.ts delete mode 100644 tests/regression/tests/issue-1268.test.ts delete mode 100644 tests/regression/tests/issue-1271.test.ts delete mode 100644 tests/regression/tests/issue-1378.test.ts delete mode 100644 tests/regression/tests/issue-1381.test.ts delete mode 100644 tests/regression/tests/issue-1388.test.ts delete mode 100644 tests/regression/tests/issue-1410.test.ts delete mode 100644 tests/regression/tests/issue-1415.test.ts delete mode 100644 tests/regression/tests/issue-1416.test.ts delete mode 100644 tests/regression/tests/issue-1427.test.ts delete mode 100644 tests/regression/tests/issue-1435.test.ts delete mode 100644 tests/regression/tests/issue-1451.test.ts delete mode 100644 tests/regression/tests/issue-1454.test.ts delete mode 100644 tests/regression/tests/issue-1466.test.ts delete mode 100644 tests/regression/tests/issue-1467.test.ts delete mode 100644 tests/regression/tests/issue-1474.test.ts delete mode 100644 tests/regression/tests/issue-1483.test.ts delete mode 100644 tests/regression/tests/issue-1487.test.ts delete mode 100644 tests/regression/tests/issue-1493.test.ts delete mode 100644 tests/regression/tests/issue-1506.test.ts delete mode 100644 tests/regression/tests/issue-1507.test.ts delete mode 100644 tests/regression/tests/issue-1518.test.ts delete mode 100644 tests/regression/tests/issue-1520.test.ts delete mode 100644 tests/regression/tests/issue-1522.test.ts delete mode 100644 tests/regression/tests/issue-1530.test.ts delete mode 100644 tests/regression/tests/issue-1533.test.ts delete mode 100644 tests/regression/tests/issue-1551.test.ts delete mode 100644 tests/regression/tests/issue-1560.test.ts delete mode 100644 tests/regression/tests/issue-1562.test.ts delete mode 100644 tests/regression/tests/issue-1563.test.ts delete mode 100644 tests/regression/tests/issue-1574.test.ts delete mode 100644 tests/regression/tests/issue-1575.test.ts delete mode 100644 tests/regression/tests/issue-1576.test.ts delete mode 100644 tests/regression/tests/issue-1585.test.ts delete mode 100644 tests/regression/tests/issue-1596.test.ts delete mode 100644 tests/regression/tests/issue-1610.test.ts delete mode 100644 tests/regression/tests/issue-1627.test.ts delete mode 100644 tests/regression/tests/issue-1642.test.ts delete mode 100644 tests/regression/tests/issue-1644.test.ts delete mode 100644 tests/regression/tests/issue-1645.test.ts delete mode 100644 tests/regression/tests/issue-1647.test.ts delete mode 100644 tests/regression/tests/issue-1648.test.ts delete mode 100644 tests/regression/tests/issue-1667.test.ts delete mode 100644 tests/regression/tests/issue-1674.test.ts delete mode 100644 tests/regression/tests/issue-1681.test.ts delete mode 100644 tests/regression/tests/issue-1693.test.ts delete mode 100644 tests/regression/tests/issue-1695.test.ts delete mode 100644 tests/regression/tests/issue-1698.test.ts delete mode 100644 tests/regression/tests/issue-1710.test.ts delete mode 100644 tests/regression/tests/issue-1734.test.ts delete mode 100644 tests/regression/tests/issue-1743.test.ts delete mode 100644 tests/regression/tests/issue-1745.test.ts delete mode 100644 tests/regression/tests/issue-1746.test.ts delete mode 100644 tests/regression/tests/issue-1755.test.ts delete mode 100644 tests/regression/tests/issue-1758.test.ts delete mode 100644 tests/regression/tests/issue-1763.test.ts delete mode 100644 tests/regression/tests/issue-177.test.ts delete mode 100644 tests/regression/tests/issue-1770.test.ts delete mode 100644 tests/regression/tests/issue-1786.test.ts delete mode 100644 tests/regression/tests/issue-1835.test.ts delete mode 100644 tests/regression/tests/issue-1843.test.ts delete mode 100644 tests/regression/tests/issue-1849.test.ts delete mode 100644 tests/regression/tests/issue-1857.test.ts delete mode 100644 tests/regression/tests/issue-1859.test.ts delete mode 100644 tests/regression/tests/issue-1870.test.ts delete mode 100644 tests/regression/tests/issue-1894.test.ts delete mode 100644 tests/regression/tests/issue-1930.test.ts delete mode 100644 tests/regression/tests/issue-1955.test.ts delete mode 100644 tests/regression/tests/issue-1964.test.ts delete mode 100644 tests/regression/tests/issue-1978.test.ts delete mode 100644 tests/regression/tests/issue-1984.test.ts delete mode 100644 tests/regression/tests/issue-1991.test.ts delete mode 100644 tests/regression/tests/issue-1992.test.ts delete mode 100644 tests/regression/tests/issue-1993.test.ts delete mode 100644 tests/regression/tests/issue-1994.test.ts delete mode 100644 tests/regression/tests/issue-1997.test.ts delete mode 100644 tests/regression/tests/issue-1998.test.ts delete mode 100644 tests/regression/tests/issue-2000.test.ts delete mode 100644 tests/regression/tests/issue-2007.test.ts delete mode 100644 tests/regression/tests/issue-2014.test.ts delete mode 100644 tests/regression/tests/issue-2019.test.ts delete mode 100644 tests/regression/tests/issue-2025.test.ts delete mode 100644 tests/regression/tests/issue-2028.test.ts delete mode 100644 tests/regression/tests/issue-2038.test.ts delete mode 100644 tests/regression/tests/issue-2039.test.ts delete mode 100644 tests/regression/tests/issue-2065.test.ts delete mode 100644 tests/regression/tests/issue-2106.test.ts delete mode 100644 tests/regression/tests/issue-2117.test.ts delete mode 100644 tests/regression/tests/issue-2168.test.ts delete mode 100644 tests/regression/tests/issue-2175.test.ts delete mode 100644 tests/regression/tests/issue-2226.test.ts delete mode 100644 tests/regression/tests/issue-2246.test.ts delete mode 100644 tests/regression/tests/issue-2283/dev.db delete mode 100644 tests/regression/tests/issue-2283/regression.test.ts delete mode 100644 tests/regression/tests/issue-2291.test.ts delete mode 100644 tests/regression/tests/issue-2294.test.ts delete mode 100644 tests/regression/tests/issue-416.test.ts delete mode 100644 tests/regression/tests/issue-646.test.ts delete mode 100644 tests/regression/tests/issue-657.test.ts delete mode 100644 tests/regression/tests/issue-665.test.ts delete mode 100644 tests/regression/tests/issue-674.test.ts delete mode 100644 tests/regression/tests/issue-689.test.ts delete mode 100644 tests/regression/tests/issue-703.test.ts delete mode 100644 tests/regression/tests/issue-714.test.ts delete mode 100644 tests/regression/tests/issue-724.test.ts delete mode 100644 tests/regression/tests/issue-735.test.ts delete mode 100644 tests/regression/tests/issue-744.test.ts delete mode 100644 tests/regression/tests/issue-756.test.ts delete mode 100644 tests/regression/tests/issue-764.test.ts delete mode 100644 tests/regression/tests/issue-765.test.ts delete mode 100644 tests/regression/tests/issue-804.test.ts delete mode 100644 tests/regression/tests/issue-811.test.ts delete mode 100644 tests/regression/tests/issue-814.test.ts delete mode 100644 tests/regression/tests/issue-825.test.ts delete mode 100644 tests/regression/tests/issue-864.test.ts delete mode 100644 tests/regression/tests/issue-886.test.ts delete mode 100644 tests/regression/tests/issue-925.test.ts delete mode 100644 tests/regression/tests/issue-947.test.ts delete mode 100644 tests/regression/tests/issue-961.test.ts delete mode 100644 tests/regression/tests/issue-965.test.ts delete mode 100644 tests/regression/tests/issue-971.test.ts delete mode 100644 tests/regression/tests/issue-992.test.ts delete mode 100644 tests/regression/tests/issue-foo.test.ts delete mode 100644 tests/regression/tests/issue-prisma-extension.test.ts delete mode 100644 tests/regression/tests/issues.test.ts delete mode 100644 tsconfig.base.json diff --git a/.devcontainer/.env b/.devcontainer/.env deleted file mode 100644 index d1016e040..000000000 --- a/.devcontainer/.env +++ /dev/null @@ -1,11 +0,0 @@ -POSTGRES_USER=postgres -POSTGRES_PASSWORD=abc123 -POSTGRES_DB=postgres -POSTGRES_HOST=postgres -POSTGRES_PORT=5432 - -ZENSTACK_TEST_DB_USER=postgres -ZENSTACK_TEST_DB_PASS=abc123 -ZENSTACK_TEST_DB_NAME=postgres -ZENSTACK_TEST_DB_HOST=postgres -ZENSTACK_TEST_DB_PORT=5432 \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index f8dff6f9c..000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: zenstack - -volumes: - postgres-data: - -networks: - workspace: - external: false - -services: - workspace: - container_name: zenstack-workspace - image: mcr.microsoft.com/devcontainers/javascript-node:20 - restart: always - volumes: - - ..:/workspace:cached - env_file: ./.env - command: sleep infinity - networks: - - workspace - - postgres: - container_name: zenstack-postgres - image: postgres - restart: always - volumes: - - postgres-data:/var/lib/postgresql/ - env_file: ./.env - networks: - - workspace - ports: - - 5432:5432 - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 5 diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 884a2323e..000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -**/dist/** diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 707715244..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,19 +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", - "plugin:jest/recommended" - ], - "rules": { - "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }], - "jest/expect-expect": "off" - } -} diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 2766c310e..000000000 --- a/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -* text=auto eol=lf - -*.bat text=auto eol=crlf \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9812232d7..3ba8779da 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,9 +14,10 @@ If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** -- ZenStack version: [e.g., 2.0.0] -- Prisma version: [e.g., 5.7.0] +- ZenStack version: [e.g., 3.1.0] - Database type: [e.g. Postgresql] +- Node.js/Bun version: [e.g., 20.0.0] +- Package manager: [e.g., npm, yarn, pnpm] **Additional context** Add any other context about the problem here. diff --git a/.github/release/.release-manifest.json b/.github/release/.release-manifest.json deleted file mode 100644 index 1857a084d..000000000 --- a/.github/release/.release-manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - ".": "2.0.0-alpha.1", - "packages/ide/jetbrains": "2.0.0-alpha.2", - "packages/language": "2.0.0-alpha.2", - "packages/misc/redwood": "2.0.0-alpha.2", - "packages/plugins/openapi": "2.0.0-alpha.2", - "packages/plugins/swr": "2.0.0-alpha.2", - "packages/plugins/tanstack-query": "2.0.0-alpha.2", - "packages/plugins/trpc": "2.0.0-alpha.2", - "packages/runtime": "2.0.0-alpha.2", - "packages/sdk": "2.0.0-alpha.2", - "packages/server": "2.0.0-alpha.2", - "packages/testtools": "2.0.0-alpha.2" -} \ No newline at end of file diff --git a/.github/release/release-main-config.json b/.github/release/release-main-config.json deleted file mode 100644 index 57e614a77..000000000 --- a/.github/release/release-main-config.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "packages": { - ".": { - "package-name": "zenstack-monorepo", - "component": "Monorepo", - "exclude-paths": ["tests", ".github"] - }, - "packages/ide/jetbrains": { - "package-name": "jetbrains", - "component": "JetBrains_IDE" - }, - "packages/language": { - "package-name": "@zenstackhq/language", - "component": "Language" - }, - "packages/misc/redwood": { - "package-name": "@zenstackhq/redwood", - "component": "Redwood" - }, - "packages/plugins/openapi": { - "package-name": "@zenstackhq/openapi", - "component": "OpenAPI_Plugin" - }, - "packages/plugins/swr": { - "package-name": "@zenstackhq/swr", - "component": "SWR_Plugin" - }, - "packages/plugins/tanstack-query": { - "package-name": "@zenstackhq/tanstack-query", - "component": "Tanstack_Query_Plugin" - }, - "packages/plugins/trpc": { - "package-name": "@zenstackhq/trpc", - "component": "tRPC_Plugin" - }, - "packages/runtime": { - "package-name": "@zenstackhq/runtime", - "component": "Runtime" - }, - "packages/sdk": { - "package-name": "@zenstackhq/sdk", - "component": "SDK" - }, - "packages/server": { - "package-name": "@zenstackhq/server", - "component": "Server" - }, - "packages/testtools": { - "package-name": "@zenstackhq/testtools", - "component": "Test_Tools" - } - }, - "include-component-in-tag": false, - "pull-request-footer": "This PR was generated by [Release-Please](https://github.com/googleapis/release-please).", - "prerelease": true, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true, - "sequential-calls": true, - "separate-pull-requests": false, - "versioning": "prerelease", - "release-type": "node", - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" -} diff --git a/.github/workflows/config/codeql-config.yml b/.github/workflows/config/codeql-config.yml index 17504a392..c72655d1e 100644 --- a/.github/workflows/config/codeql-config.yml +++ b/.github/workflows/config/codeql-config.yml @@ -5,4 +5,3 @@ paths-ignore: - '**/*.test.ts' - '**/*.test.tsx' - '**/__tests__/**' - - 'packages/ide/**' diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml deleted file mode 100644 index 8683f346b..000000000 --- a/.github/workflows/integration-test.yml +++ /dev/null @@ -1,96 +0,0 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - -name: Integration Tests - -env: - TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }} - DO_NOT_TRACK: '1' - -on: - pull_request: - branches: - - main - - dev - - release/* - -permissions: - contents: read - -jobs: - build-test: - runs-on: buildjet-8vcpu-ubuntu-2204 - - services: - postgres: - image: postgres - env: - POSTGRES_PASSWORD: abc123 - # Set health checks to wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - strategy: - matrix: - node-version: [20.x] - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 9.15.9 - - - name: Use Node.js ${{ matrix.node-version }} - uses: buildjet/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: buildjet/cache@v3 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Gradle Wrapper Validation - uses: gradle/wrapper-validation-action@v1.1.0 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: zulu - java-version: 17 - - - name: Setup Gradle - uses: gradle/gradle-build-action@v2.4.2 - with: - gradle-home-cache-cleanup: true - - - name: Build - run: DEFAULT_NPM_TAG=latest pnpm run build - - # install again for internal dependencies - - name: Install internal dependencies - run: pnpm install --frozen-lockfile - - - name: Integration Test - run: pnpm run test-scaffold && pnpm run test-integration diff --git a/.github/workflows/regression-test.yml b/.github/workflows/regression-test.yml deleted file mode 100644 index 636639c6e..000000000 --- a/.github/workflows/regression-test.yml +++ /dev/null @@ -1,96 +0,0 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - -name: Regression Tests - -env: - TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }} - DO_NOT_TRACK: '1' - -on: - pull_request: - branches: - - main - - dev - - release/* - -permissions: - contents: read - -jobs: - build-test: - runs-on: buildjet-8vcpu-ubuntu-2204 - - services: - postgres: - image: postgres - env: - POSTGRES_PASSWORD: abc123 - # Set health checks to wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - strategy: - matrix: - node-version: [20.x] - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 9.15.9 - - - name: Use Node.js ${{ matrix.node-version }} - uses: buildjet/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: buildjet/cache@v3 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Gradle Wrapper Validation - uses: gradle/wrapper-validation-action@v1.1.0 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: zulu - java-version: 17 - - - name: Setup Gradle - uses: gradle/gradle-build-action@v2.4.2 - with: - gradle-home-cache-cleanup: true - - - name: Build - run: DEFAULT_NPM_TAG=latest pnpm run build - - # install again for internal dependencies - - name: Install internal dependencies - run: pnpm install --frozen-lockfile - - - name: Regression Test - run: pnpm run test-scaffold && pnpm run test-regression diff --git a/.github/workflows/security-dependency-review.yml b/.github/workflows/security-dependency-review.yml deleted file mode 100644 index 09018a429..000000000 --- a/.github/workflows/security-dependency-review.yml +++ /dev/null @@ -1,33 +0,0 @@ -# Dependency Review Action -# -# This Action will scan dependency manifest files that change as part of a Pull Request, -# surfacing known-vulnerable versions of the packages declared or updated in the PR. -# Once installed, if the workflow run is marked as required, -# PRs introducing known-vulnerable packages will be blocked from merging. -# -# Source repository: https://github.com/actions/dependency-review-action -name: Security - Dependency Review -on: - merge_group: - pull_request: - -permissions: - contents: read - -jobs: - dependency-review: - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@v2.6.1 - with: - egress-policy: audit - - # checks out the repository - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. - - - name: 'Dependency Review' - uses: actions/dependency-review-action@v2.5.1 diff --git a/.github/workflows/security-ossar.yml b/.github/workflows/security-ossar.yml deleted file mode 100644 index 244f2b147..000000000 --- a/.github/workflows/security-ossar.yml +++ /dev/null @@ -1,74 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow integrates a collection of open source static analysis tools -# with GitHub code scanning. For documentation, or to provide feedback, visit -# https://github.com/github/ossar-action -name: Security - OSSAR - -on: - merge_group: - push: - branches: - - main - - dev - - release/* - - v2 - pull_request: - branches: - - main - - dev - - release/* - - v2 - schedule: - - cron: '41 3 * * 5' - -permissions: - contents: read - -jobs: - OSSAR-Scan: - runs-on: windows-latest - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - - steps: - - name: Harden Runner - uses: step-security/harden-runner@v2.6.1 - with: - egress-policy: audit - - - name: Workflow Telemetry - uses: catchpoint/workflow-telemetry-action@v1.8.7 - with: - github_token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. - comment_on_pr: false - theme: dark - proc_trace_sys_enable: true - - # checks out the repository - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. - - - uses: actions/setup-dotnet@v3.2.0 - with: - dotnet-version: | - 5.0.x - 6.0.x - - # Run open source static analysis tools - - name: Run OSSAR - uses: github/ossar-action@v1 - id: ossar - - # Upload results to the Security tab - - name: Upload OSSAR results - uses: github/codeql-action/upload-sarif@v2.22.12 - with: - sarif_file: ${{ steps.ossar.outputs.sarifFile }} diff --git a/.github/workflows/security-scorecard.yml b/.github/workflows/security-scorecard.yml deleted file mode 100644 index c80226279..000000000 --- a/.github/workflows/security-scorecard.yml +++ /dev/null @@ -1,68 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. They are provided -# by a third-party and are governed by separate terms of service, privacy -# policy, and support documentation. - -name: Security - Scorecard supply-chain security -on: - # For Branch-Protection check. Only the default branch is supported. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection - branch_protection_rule: - # To guarantee Maintained check is occasionally updated. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained - schedule: - - cron: '21 9 * * 6' - push: - branches: - - main - -# Declare default permissions as read only. -permissions: - contents: read - -jobs: - analysis: - name: Scorecard analysis - runs-on: ubuntu-latest - permissions: - # Needed to upload the results to code-scanning dashboard. - security-events: write - # Needed to publish results and get a badge (see publish_results below). - id-token: write - # Uncomment the permissions below if installing in a private repository. - # contents: read - # actions: read - - steps: - - name: Harden Runner - uses: step-security/harden-runner@v2.6.1 - with: - egress-policy: audit - - # checks out the repository - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. - - - name: 'Run analysis' - uses: ossf/scorecard-action@v2.3.3 - with: - results_file: results.sarif - results_format: sarif - repo_token: ${{ secrets.BOT_TOKEN || github.token }} # Bot Token is a PAT for a automation account. - publish_results: true - - # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF - # format to the repository Actions tab. - - name: 'Upload artifact' - uses: actions/upload-artifact@v4 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - # Upload the results to GitHub's code scanning dashboard. - - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@v2.2.4 - with: - sarif_file: results.sarif diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 308555701..000000000 --- a/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -auto-install-peers=true -git-checks=false -node-linker=hoisted diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 14a85cdea..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,85 +0,0 @@ -# 0.5.0 (2022-12-15) - -### Features - -- Serialization between client (hooks) and server now uses [superjson](https://github.com/blitz-js/superjson), [[#139](https://github.com/zenstackhq/zenstack/issues/139)] - -### Fixes and improvements - -- Fixed goto definition issue in VSCode extension, [[#69](https://github.com/zenstackhq/zenstack/issues/69)] - -### Breaking changes - -- Next-auth adapter and helper are moved to a separate package `@zenstackhq/next-auth`. - -# 0.4.0 (2022-12-01) - -### Features - -- `zenstack init` command for initializing a project, [#109](https://github.com/zenstackhq/zenstack/issues/109), [doc](https://zenstack.dev/#/quick-start?id=adding-to-an-existing-project). - -- Field constraint suport, [#94](https://github.com/zenstackhq/zenstack/issues/94), [doc](https://zenstack.dev/#/zmodel-field-constraint). - -- Support for server-side CRUD with access policy check (SSR), [#126](https://github.com/zenstackhq/zenstack/issues/126), [doc](https://zenstack.dev/#/server-side-rendering). - -- Options for disabling fetching in hooks (useful when arguments are not ready), [#57](https://github.com/zenstackhq/zenstack/issues/57), [doc](https://zenstack.dev/#/runtime-api?id=requestoptions). - -- Telemetry in CLI, [#102](https://github.com/zenstackhq/zenstack/issues/102), [doc](https://zenstack.dev/#/telemetry). - -- Iron-session based starter, [#95](https://github.com/zenstackhq/zenstack/issues/95), [link](https://github.com/zenstackhq/nextjs-iron-session-starter). - -- Barebone starter (without authentication), [link](https://github.com/zenstackhq/nextjs-barebone-starter). - -- [Website](https://zenstack.dev) is live! - -### Fixes and improvements - -- Merge `@zenstackhq/internal` into `@zenstackhq/runtime` so as to have a single runtime dependency, [#70](https://github.com/zenstackhq/zenstack/issues/70). - -- More accurate log for access policy violation, [#71](https://github.com/zenstackhq/zenstack/issues/71). - -- `auth()` function's return type is now resolved to `User` model in ZModel, instead of `Any`, [#65](https://github.com/zenstackhq/zenstack/issues/65). - -- Improved ZModel type checking, [#67](https://github.com/zenstackhq/zenstack/issues/67), [#46](https://github.com/zenstackhq/zenstack/issues/46), [#99](https://github.com/zenstackhq/zenstack/issues/99). - -- Upgraded to Prisma 4.7. - -### Breaking changes - -- @zenstackhq/runtime doesn't export anything now. - - Use @zenstackhq/runtime/types for type definitions shared between client and server, @zenstackhq/runtime/client for client-specific libaries (like React hooks), and @zenstackhq/runtime/server for server-specific libraries. - -# 0.3.0 (2022-11-08) - -### Features - -- `@password` and `@omit` attribute support - -- Configurable logging (to stdout and emitting as events) - -### Fixes and improvements - -- More robust policy checks - -- Properly handles complex types like BigInt, Date, Decimal, etc. - -- Makes sure Prisma schema is regenerated for related CLI commands - -- Lower VSCode engine version requirement for the extension - -- Better overall documentation - -# 0.2.0 (2022-10-29) - -### Features - -- `ZModel` data modeling schema (an extension to [Prisma Schema](https://www.prisma.io/docs/concepts/components/prisma-schema)) - -- `zenstack` cli for generating RESTful services, auth adapters and React hooks from `ZModel` - -- Policy engine that transforms policy rules into Prisma query conditions - -- Runtime packages - -- An initial set of tests diff --git a/SECURITY.md b/SECURITY.md index d79cbe67e..2d74e8769 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | ------- | ------------------ | -| >=1.0.0 | :white_check_mark: | +| >=2.0.0 | :white_check_mark: | ## Reporting a Vulnerability diff --git a/jest.config.ts b/jest.config.ts deleted file mode 100644 index b08a6426f..000000000 --- a/jest.config.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property and type check, visit: - * https://jestjs.io/docs/configuration - */ - -import path from 'path'; - -export default { - // Automatically clear mock calls, instances, contexts and results before every test - clearMocks: true, - - globalSetup: path.join(__dirname, './script/test-global-setup.ts'), - - setupFiles: [path.join(__dirname, './script/set-test-env.ts')], - - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, - - // The directory where Jest should output its coverage files - coverageDirectory: path.join(__dirname, '.test/coverage'), - - // An array of regexp pattern strings used to skip coverage collection - coveragePathIgnorePatterns: ['/node_modules/', '/tests/'], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'v8', - - // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: ['json', 'text', 'lcov', 'clover'], - - // A map from regular expressions to paths to transformers - transform: { '^.+\\.tsx?$': 'ts-jest' }, - - testTimeout: 300000, -}; diff --git a/packages/LICENSE b/packages/LICENSE deleted file mode 100644 index 62f71afa1..000000000 --- a/packages/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022-2025 ZenStack - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/ide/jetbrains/.gitignore b/packages/ide/jetbrains/.gitignore deleted file mode 100644 index 80b28c077..000000000 --- a/packages/ide/jetbrains/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ -.sign diff --git a/packages/ide/jetbrains/.idea/.gitignore b/packages/ide/jetbrains/.idea/.gitignore deleted file mode 100644 index 13566b81b..000000000 --- a/packages/ide/jetbrains/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/packages/ide/jetbrains/.idea/.name b/packages/ide/jetbrains/.idea/.name deleted file mode 100644 index 8aa065d41..000000000 --- a/packages/ide/jetbrains/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -zenstack \ No newline at end of file diff --git a/packages/ide/jetbrains/.idea/gradle.xml b/packages/ide/jetbrains/.idea/gradle.xml deleted file mode 100644 index 42862bf11..000000000 --- a/packages/ide/jetbrains/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/packages/ide/jetbrains/.idea/kotlinc.xml b/packages/ide/jetbrains/.idea/kotlinc.xml deleted file mode 100644 index ae3f30ae1..000000000 --- a/packages/ide/jetbrains/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/ide/jetbrains/.idea/misc.xml b/packages/ide/jetbrains/.idea/misc.xml deleted file mode 100644 index 510c2cda9..000000000 --- a/packages/ide/jetbrains/.idea/misc.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/packages/ide/jetbrains/.idea/vcs.xml b/packages/ide/jetbrains/.idea/vcs.xml deleted file mode 100644 index c2365ab11..000000000 --- a/packages/ide/jetbrains/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/ide/jetbrains/CHANGELOG.md b/packages/ide/jetbrains/CHANGELOG.md deleted file mode 100644 index 81e2c8542..000000000 --- a/packages/ide/jetbrains/CHANGELOG.md +++ /dev/null @@ -1,138 +0,0 @@ -# Changelog - -## [Unreleased] - -### Fixed - -- Update JetBrains platform version compatibility. - -## 2.18.0 - -### Fixed - -- Views are not required to have a unique identifying field marked with `@id` or `@unique`. - -## 2.17.0 - -### Fixed - -- Support `@db.Json` and `@db.JsonB` attributes on strongly typed JSON fields. - -## 2.16.0 - -### Fixed - -- Prefer to use "stdlib.zmodel" from user's node_modules folder. - -## 2.13.0 - -### Added - -- Support `@default` for `@json` fields. - -### Fixed - -## 2.12.1 - -### Added - -- Validating regex patterns in ZModel. - -## 2.12.0 - -### Added - -- Field encryption attribute `@encrypted`. - -## 2.9.3 - -### Fixed - -- Proper semantic highlighting and formatting for type declarations. - -## 2.9.0 - -### Added - -- Support for using `@@validate` attribute inside type declarations. - -## 2.8.1 - -### Fixed - -- Wrong validation errors when using strongly typed JSON fields in a multi-file schema setup. - -## 2.8.0 - -### Added - -- Type declaration support. - -## 2.7.0 - -### Fixed - -- ZModel validation issues importing zmodel files from npm packages. - -## 2.6.0 - -### Fixed - -- ZModel validation issues when accessing fields defined in a base model from `future().` or `this.`. - -## 2.5.0 - -### Added - -- A new `path` parameter to the `@@validate` attribute for providing an optional path to the field that caused the error. - -## 2.4.0 - -### Added - -- The `uuid()` function is updated to support the new UUID version feature from Prisma. - -## 2.3.0 - -### Added - -- New `check()` policy rule function. - -### Fixed - -- Fixed the issue with formatting schemas containing `Unsupported` type. - -## 2.2.0 - -### Added - -- Support comparing fields from different models in mutation policy rules ("create", "update", and "delete). - -## 2.1.0 - -### Added - -- Support using ZModel type names (e.g., `DateTime`) as model field names. -- `auth()` is resolved from all reachable schema files. - -## 2.0.0 - -### Added - -- ZenStack V2 release! - -## 1.11.0 - -### Added - -- Added support to complex usage of `@@index` attribute like `@@index([content(ops: raw("gin_trgm_ops"))], type: Gin)`. - -### Fixed - -- Fixed several ZModel validation issues related to model inheritance. - -## 1.7.0 - -### Added - -- Auto-completion is now supported inside attributes. diff --git a/packages/ide/jetbrains/build.gradle.kts b/packages/ide/jetbrains/build.gradle.kts deleted file mode 100644 index d48d18452..000000000 --- a/packages/ide/jetbrains/build.gradle.kts +++ /dev/null @@ -1,99 +0,0 @@ -import org.jetbrains.changelog.Changelog -import org.jetbrains.changelog.date - -plugins { - id("java") - id("org.jetbrains.kotlin.jvm") version "1.9.21" - id("org.jetbrains.intellij") version "1.16.1" - id("org.jetbrains.changelog") version "2.2.0" -} - -group = "dev.zenstack" -version = "2.22.2" - -repositories { - mavenCentral() -} - -// Configure Gradle IntelliJ Plugin -// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html -intellij { - version.set("2023.3.2") - type.set("IU") // Target IDE Platform - - plugins.set(listOf("JavaScript", "org.jetbrains.plugins.textmate")) -} - -tasks { - // Set the JVM compatibility versions - withType { - sourceCompatibility = "17" - targetCompatibility = "17" - } - withType { - kotlinOptions.jvmTarget = "17" - } - - prepareSandbox { - doLast { - copy { - from("${project.projectDir}/../../schema/bundle/language-server/main.js") - into("${destinationDir.path}/zenstack/language-server/") - } - copy { - from("${project.projectDir}/../../schema/src/res/stdlib.zmodel") - into("${destinationDir.path}/zenstack/res/") - } - copy { - from("${project.projectDir}/src/main/textMate/zmodel.tmbundle") - into("${destinationDir.path}/zenstack/res/zmodel.tmbundle") - } - copy { - from("${project.projectDir}/../../language/syntaxes/zmodel.tmLanguage") - into("${destinationDir.path}/zenstack/res/zmodel.tmbundle/Syntaxes/") - } - } - } - - patchPluginXml { - sinceBuild.set("233.2") - untilBuild.set("271.*") - changeNotes.set(provider { - changelog.renderItem( - changelog - .getUnreleased() - .withHeader(false) - .withEmptySections(false), - Changelog.OutputType.HTML - ) - }) - } - - signPlugin { - certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) - privateKey.set(System.getenv("PRIVATE_KEY")) - password.set(System.getenv("PRIVATE_KEY_PASSWORD")) - } - - publishPlugin { - token.set(System.getenv("PUBLISH_TOKEN")) - } -} - -changelog { - header.set(provider { "[${version.get()}] - ${date()}" }) - introduction.set( - """ - [ZenStack](https://zenstack.dev) is a toolkit that simplifies the development of a web app's backend. This plugin provides code editing experiences for its ZModel schema language. - - ## Features - - - Syntax highlighting - - Error highlighting - - Go to definition - - Code completion - - Formatting - """.trimIndent() - ) - groups.set(listOf("Added", "Changed", "Deprecated", "Removed", "Fixed", "Security")) -} diff --git a/packages/ide/jetbrains/gradle.properties b/packages/ide/jetbrains/gradle.properties deleted file mode 100644 index e1c1990f8..000000000 --- a/packages/ide/jetbrains/gradle.properties +++ /dev/null @@ -1,6 +0,0 @@ -# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib -kotlin.stdlib.default.dependency=false -# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html -org.gradle.configuration-cache=false -# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html -org.gradle.caching=true diff --git a/packages/ide/jetbrains/gradle/wrapper/gradle-wrapper.jar b/packages/ide/jetbrains/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 249e5832f090a2944b7473328c07c9755baa3196..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60756 zcmb5WV{~QRw(p$^Dz@00IL3?^hro$gg*4VI_WAaTyVM5Foj~O|-84 z$;06hMwt*rV;^8iB z1~&0XWpYJmG?Ts^K9PC62H*`G}xom%S%yq|xvG~FIfP=9*f zZoDRJBm*Y0aId=qJ?7dyb)6)JGWGwe)MHeNSzhi)Ko6J<-m@v=a%NsP537lHe0R* z`If4$aaBA#S=w!2z&m>{lpTy^Lm^mg*3?M&7HFv}7K6x*cukLIGX;bQG|QWdn{%_6 zHnwBKr84#B7Z+AnBXa16a?or^R?+>$4`}{*a_>IhbjvyTtWkHw)|ay)ahWUd-qq$~ zMbh6roVsj;_qnC-R{G+Cy6bApVOinSU-;(DxUEl!i2)1EeQ9`hrfqj(nKI7?Z>Xur zoJz-a`PxkYit1HEbv|jy%~DO^13J-ut986EEG=66S}D3!L}Efp;Bez~7tNq{QsUMm zh9~(HYg1pA*=37C0}n4g&bFbQ+?-h-W}onYeE{q;cIy%eZK9wZjSwGvT+&Cgv z?~{9p(;bY_1+k|wkt_|N!@J~aoY@|U_RGoWX<;p{Nu*D*&_phw`8jYkMNpRTWx1H* z>J-Mi_!`M468#5Aix$$u1M@rJEIOc?k^QBc?T(#=n&*5eS#u*Y)?L8Ha$9wRWdH^3D4|Ps)Y?m0q~SiKiSfEkJ!=^`lJ(%W3o|CZ zSrZL-Xxc{OrmsQD&s~zPfNJOpSZUl%V8tdG%ei}lQkM+z@-4etFPR>GOH9+Y_F<3=~SXln9Kb-o~f>2a6Xz@AS3cn^;c_>lUwlK(n>z?A>NbC z`Ud8^aQy>wy=$)w;JZzA)_*Y$Z5hU=KAG&htLw1Uh00yE!|Nu{EZkch zY9O6x7Y??>!7pUNME*d!=R#s)ghr|R#41l!c?~=3CS8&zr6*aA7n9*)*PWBV2w+&I zpW1-9fr3j{VTcls1>ua}F*bbju_Xq%^v;-W~paSqlf zolj*dt`BBjHI)H9{zrkBo=B%>8}4jeBO~kWqO!~Thi!I1H(in=n^fS%nuL=X2+s!p}HfTU#NBGiwEBF^^tKU zbhhv+0dE-sbK$>J#t-J!B$TMgN@Wh5wTtK2BG}4BGfsZOoRUS#G8Cxv|6EI*n&Xxq zt{&OxCC+BNqz$9b0WM7_PyBJEVObHFh%%`~!@MNZlo*oXDCwDcFwT~Rls!aApL<)^ zbBftGKKBRhB!{?fX@l2_y~%ygNFfF(XJzHh#?`WlSL{1lKT*gJM zs>bd^H9NCxqxn(IOky5k-wALFowQr(gw%|`0991u#9jXQh?4l|l>pd6a&rx|v=fPJ z1mutj{YzpJ_gsClbWFk(G}bSlFi-6@mwoQh-XeD*j@~huW4(8ub%^I|azA)h2t#yG z7e_V_<4jlM3D(I+qX}yEtqj)cpzN*oCdYHa!nm%0t^wHm)EmFP*|FMw!tb@&`G-u~ zK)=Sf6z+BiTAI}}i{*_Ac$ffr*Wrv$F7_0gJkjx;@)XjYSh`RjAgrCck`x!zP>Ifu z&%he4P|S)H*(9oB4uvH67^0}I-_ye_!w)u3v2+EY>eD3#8QR24<;7?*hj8k~rS)~7 zSXs5ww)T(0eHSp$hEIBnW|Iun<_i`}VE0Nc$|-R}wlSIs5pV{g_Dar(Zz<4X3`W?K z6&CAIl4U(Qk-tTcK{|zYF6QG5ArrEB!;5s?tW7 zrE3hcFY&k)+)e{+YOJ0X2uDE_hd2{|m_dC}kgEKqiE9Q^A-+>2UonB+L@v3$9?AYw zVQv?X*pK;X4Ovc6Ev5Gbg{{Eu*7{N3#0@9oMI~}KnObQE#Y{&3mM4`w%wN+xrKYgD zB-ay0Q}m{QI;iY`s1Z^NqIkjrTlf`B)B#MajZ#9u41oRBC1oM1vq0i|F59> z#StM@bHt|#`2)cpl_rWB($DNJ3Lap}QM-+A$3pe}NyP(@+i1>o^fe-oxX#Bt`mcQc zb?pD4W%#ep|3%CHAYnr*^M6Czg>~L4?l16H1OozM{P*en298b+`i4$|w$|4AHbzqB zHpYUsHZET$Z0ztC;U+0*+amF!@PI%^oUIZy{`L{%O^i{Xk}X0&nl)n~tVEpcAJSJ} zverw15zP1P-O8h9nd!&hj$zuwjg?DoxYIw{jWM zW5_pj+wFy8Tsa9g<7Qa21WaV&;ejoYflRKcz?#fSH_)@*QVlN2l4(QNk| z4aPnv&mrS&0|6NHq05XQw$J^RR9T{3SOcMKCXIR1iSf+xJ0E_Wv?jEc*I#ZPzyJN2 zUG0UOXHl+PikM*&g$U@g+KbG-RY>uaIl&DEtw_Q=FYq?etc!;hEC_}UX{eyh%dw2V zTTSlap&5>PY{6I#(6`j-9`D&I#|YPP8a;(sOzgeKDWsLa!i-$frD>zr-oid!Hf&yS z!i^cr&7tN}OOGmX2)`8k?Tn!!4=tz~3hCTq_9CdiV!NIblUDxHh(FJ$zs)B2(t5@u z-`^RA1ShrLCkg0)OhfoM;4Z{&oZmAec$qV@ zGQ(7(!CBk<5;Ar%DLJ0p0!ResC#U<+3i<|vib1?{5gCebG7$F7URKZXuX-2WgF>YJ^i zMhHDBsh9PDU8dlZ$yJKtc6JA#y!y$57%sE>4Nt+wF1lfNIWyA`=hF=9Gj%sRwi@vd z%2eVV3y&dvAgyuJ=eNJR+*080dbO_t@BFJO<@&#yqTK&+xc|FRR;p;KVk@J3$S{p` zGaMj6isho#%m)?pOG^G0mzOAw0z?!AEMsv=0T>WWcE>??WS=fII$t$(^PDPMU(P>o z_*0s^W#|x)%tx8jIgZY~A2yG;US0m2ZOQt6yJqW@XNY_>_R7(Nxb8Ged6BdYW6{prd!|zuX$@Q2o6Ona8zzYC1u!+2!Y$Jc9a;wy+pXt}o6~Bu1oF1c zp7Y|SBTNi@=I(K%A60PMjM#sfH$y*c{xUgeSpi#HB`?|`!Tb&-qJ3;vxS!TIzuTZs-&%#bAkAyw9m4PJgvey zM5?up*b}eDEY+#@tKec)-c(#QF0P?MRlD1+7%Yk*jW;)`f;0a-ZJ6CQA?E%>i2Dt7T9?s|9ZF|KP4;CNWvaVKZ+Qeut;Jith_y{v*Ny6Co6!8MZx;Wgo z=qAi%&S;8J{iyD&>3CLCQdTX*$+Rx1AwA*D_J^0>suTgBMBb=*hefV+Ars#mmr+YsI3#!F@Xc1t4F-gB@6aoyT+5O(qMz*zG<9Qq*f0w^V!03rpr*-WLH}; zfM{xSPJeu6D(%8HU%0GEa%waFHE$G?FH^kMS-&I3)ycx|iv{T6Wx}9$$D&6{%1N_8 z_CLw)_9+O4&u94##vI9b-HHm_95m)fa??q07`DniVjAy`t7;)4NpeyAY(aAk(+T_O z1om+b5K2g_B&b2DCTK<>SE$Ode1DopAi)xaJjU>**AJK3hZrnhEQ9E`2=|HHe<^tv z63e(bn#fMWuz>4erc47}!J>U58%<&N<6AOAewyzNTqi7hJc|X{782&cM zHZYclNbBwU6673=!ClmxMfkC$(CykGR@10F!zN1Se83LR&a~$Ht&>~43OX22mt7tcZUpa;9@q}KDX3O&Ugp6< zLZLfIMO5;pTee1vNyVC$FGxzK2f>0Z-6hM82zKg44nWo|n}$Zk6&;5ry3`(JFEX$q zK&KivAe${e^5ZGc3a9hOt|!UOE&OocpVryE$Y4sPcs4rJ>>Kbi2_subQ9($2VN(3o zb~tEzMsHaBmBtaHAyES+d3A(qURgiskSSwUc9CfJ@99&MKp2sooSYZu+-0t0+L*!I zYagjOlPgx|lep9tiU%ts&McF6b0VE57%E0Ho%2oi?=Ks+5%aj#au^OBwNwhec zta6QAeQI^V!dF1C)>RHAmB`HnxyqWx?td@4sd15zPd*Fc9hpDXP23kbBenBxGeD$k z;%0VBQEJ-C)&dTAw_yW@k0u?IUk*NrkJ)(XEeI z9Y>6Vel>#s_v@=@0<{4A{pl=9cQ&Iah0iD0H`q)7NeCIRz8zx;! z^OO;1+IqoQNak&pV`qKW+K0^Hqp!~gSohcyS)?^P`JNZXw@gc6{A3OLZ?@1Uc^I2v z+X!^R*HCm3{7JPq{8*Tn>5;B|X7n4QQ0Bs79uTU%nbqOJh`nX(BVj!#f;#J+WZxx4 z_yM&1Y`2XzhfqkIMO7tB3raJKQS+H5F%o83bM+hxbQ zeeJm=Dvix$2j|b4?mDacb67v-1^lTp${z=jc1=j~QD>7c*@+1?py>%Kj%Ejp7Y-!? z8iYRUlGVrQPandAaxFfks53@2EC#0)%mrnmGRn&>=$H$S8q|kE_iWko4`^vCS2aWg z#!`RHUGyOt*k?bBYu3*j3u0gB#v(3tsije zgIuNNWNtrOkx@Pzs;A9un+2LX!zw+p3_NX^Sh09HZAf>m8l@O*rXy_82aWT$Q>iyy zqO7Of)D=wcSn!0+467&!Hl))eff=$aneB?R!YykdKW@k^_uR!+Q1tR)+IJb`-6=jj zymzA>Sv4>Z&g&WWu#|~GcP7qP&m*w-S$)7Xr;(duqCTe7p8H3k5>Y-n8438+%^9~K z3r^LIT_K{i7DgEJjIocw_6d0!<;wKT`X;&vv+&msmhAAnIe!OTdybPctzcEzBy88_ zWO{6i4YT%e4^WQZB)KHCvA(0tS zHu_Bg+6Ko%a9~$EjRB90`P(2~6uI@SFibxct{H#o&y40MdiXblu@VFXbhz>Nko;7R z70Ntmm-FePqhb%9gL+7U8@(ch|JfH5Fm)5${8|`Lef>LttM_iww6LW2X61ldBmG0z zax3y)njFe>j*T{i0s8D4=L>X^j0)({R5lMGVS#7(2C9@AxL&C-lZQx~czI7Iv+{%1 z2hEG>RzX4S8x3v#9sgGAnPzptM)g&LB}@%E>fy0vGSa(&q0ch|=ncKjNrK z`jA~jObJhrJ^ri|-)J^HUyeZXz~XkBp$VhcTEcTdc#a2EUOGVX?@mYx#Vy*!qO$Jv zQ4rgOJ~M*o-_Wptam=~krnmG*p^j!JAqoQ%+YsDFW7Cc9M%YPiBOrVcD^RY>m9Pd< zu}#9M?K{+;UIO!D9qOpq9yxUquQRmQNMo0pT`@$pVt=rMvyX)ph(-CCJLvUJy71DI zBk7oc7)-%ngdj~s@76Yse3L^gV0 z2==qfp&Q~L(+%RHP0n}+xH#k(hPRx(!AdBM$JCfJ5*C=K3ts>P?@@SZ_+{U2qFZb>4kZ{Go37{# zSQc+-dq*a-Vy4?taS&{Ht|MLRiS)Sn14JOONyXqPNnpq&2y~)6wEG0oNy>qvod$FF z`9o&?&6uZjhZ4_*5qWVrEfu(>_n2Xi2{@Gz9MZ8!YmjYvIMasE9yVQL10NBrTCczq zcTY1q^PF2l!Eraguf{+PtHV3=2A?Cu&NN&a8V(y;q(^_mFc6)%Yfn&X&~Pq zU1?qCj^LF(EQB1F`8NxNjyV%fde}dEa(Hx=r7$~ts2dzDwyi6ByBAIx$NllB4%K=O z$AHz1<2bTUb>(MCVPpK(E9wlLElo(aSd(Os)^Raum`d(g9Vd_+Bf&V;l=@mM=cC>) z)9b0enb)u_7V!!E_bl>u5nf&Rl|2r=2F3rHMdb7y9E}}F82^$Rf+P8%dKnOeKh1vs zhH^P*4Ydr^$)$h@4KVzxrHyy#cKmWEa9P5DJ|- zG;!Qi35Tp7XNj60=$!S6U#!(${6hyh7d4q=pF{`0t|N^|L^d8pD{O9@tF~W;#Je*P z&ah%W!KOIN;SyAEhAeTafJ4uEL`(RtnovM+cb(O#>xQnk?dzAjG^~4$dFn^<@-Na3 z395;wBnS{t*H;Jef2eE!2}u5Ns{AHj>WYZDgQJt8v%x?9{MXqJsGP|l%OiZqQ1aB! z%E=*Ig`(!tHh>}4_z5IMpg{49UvD*Pp9!pxt_gdAW%sIf3k6CTycOT1McPl=_#0?8 zVjz8Hj*Vy9c5-krd-{BQ{6Xy|P$6LJvMuX$* zA+@I_66_ET5l2&gk9n4$1M3LN8(yEViRx&mtd#LD}AqEs?RW=xKC(OCWH;~>(X6h!uDxXIPH06xh z*`F4cVlbDP`A)-fzf>MuScYsmq&1LUMGaQ3bRm6i7OsJ|%uhTDT zlvZA1M}nz*SalJWNT|`dBm1$xlaA>CCiQ zK`xD-RuEn>-`Z?M{1%@wewf#8?F|(@1e0+T4>nmlSRrNK5f)BJ2H*$q(H>zGD0>eL zQ!tl_Wk)k*e6v^m*{~A;@6+JGeWU-q9>?+L_#UNT%G?4&BnOgvm9@o7l?ov~XL+et zbGT)|G7)KAeqb=wHSPk+J1bdg7N3$vp(ekjI1D9V$G5Cj!=R2w=3*4!z*J-r-cyeb zd(i2KmX!|Lhey!snRw z?#$Gu%S^SQEKt&kep)up#j&9}e+3=JJBS(s>MH+|=R(`8xK{mmndWo_r`-w1#SeRD&YtAJ#GiVI*TkQZ}&aq<+bU2+coU3!jCI6E+Ad_xFW*ghnZ$q zAoF*i&3n1j#?B8x;kjSJD${1jdRB;)R*)Ao!9bd|C7{;iqDo|T&>KSh6*hCD!rwv= zyK#F@2+cv3=|S1Kef(E6Niv8kyLVLX&e=U;{0x{$tDfShqkjUME>f8d(5nzSkY6@! z^-0>DM)wa&%m#UF1F?zR`8Y3X#tA!*7Q$P3lZJ%*KNlrk_uaPkxw~ zxZ1qlE;Zo;nb@!SMazSjM>;34ROOoygo%SF);LL>rRonWwR>bmSd1XD^~sGSu$Gg# zFZ`|yKU0%!v07dz^v(tY%;So(e`o{ZYTX`hm;@b0%8|H>VW`*cr8R%3n|ehw2`(9B+V72`>SY}9^8oh$En80mZK9T4abVG*to;E z1_S6bgDOW?!Oy1LwYy=w3q~KKdbNtyH#d24PFjX)KYMY93{3-mPP-H>@M-_>N~DDu zENh~reh?JBAK=TFN-SfDfT^=+{w4ea2KNWXq2Y<;?(gf(FgVp8Zp-oEjKzB%2Iqj;48GmY3h=bcdYJ}~&4tS`Q1sb=^emaW$IC$|R+r-8V- zf0$gGE(CS_n4s>oicVk)MfvVg#I>iDvf~Ov8bk}sSxluG!6#^Z_zhB&U^`eIi1@j( z^CK$z^stBHtaDDHxn+R;3u+>Lil^}fj?7eaGB z&5nl^STqcaBxI@v>%zG|j))G(rVa4aY=B@^2{TFkW~YP!8!9TG#(-nOf^^X-%m9{Z zCC?iC`G-^RcBSCuk=Z`(FaUUe?hf3{0C>>$?Vs z`2Uud9M+T&KB6o4o9kvdi^Q=Bw!asPdxbe#W-Oaa#_NP(qpyF@bVxv5D5))srkU#m zj_KA+#7sqDn*Ipf!F5Byco4HOSd!Ui$l94|IbW%Ny(s1>f4|Mv^#NfB31N~kya9!k zWCGL-$0ZQztBate^fd>R!hXY_N9ZjYp3V~4_V z#eB)Kjr8yW=+oG)BuNdZG?jaZlw+l_ma8aET(s+-x+=F-t#Qoiuu1i`^x8Sj>b^U} zs^z<()YMFP7CmjUC@M=&lA5W7t&cxTlzJAts*%PBDAPuqcV5o7HEnqjif_7xGt)F% zGx2b4w{@!tE)$p=l3&?Bf#`+!-RLOleeRk3 z7#pF|w@6_sBmn1nECqdunmG^}pr5(ZJQVvAt$6p3H(16~;vO>?sTE`Y+mq5YP&PBo zvq!7#W$Gewy`;%6o^!Dtjz~x)T}Bdk*BS#=EY=ODD&B=V6TD2z^hj1m5^d6s)D*wk zu$z~D7QuZ2b?5`p)E8e2_L38v3WE{V`bVk;6fl#o2`) z99JsWhh?$oVRn@$S#)uK&8DL8>An0&S<%V8hnGD7Z^;Y(%6;^9!7kDQ5bjR_V+~wp zfx4m3z6CWmmZ<8gDGUyg3>t8wgJ5NkkiEm^(sedCicP^&3D%}6LtIUq>mXCAt{9eF zNXL$kGcoUTf_Lhm`t;hD-SE)m=iBnxRU(NyL}f6~1uH)`K!hmYZjLI%H}AmEF5RZt z06$wn63GHnApHXZZJ}s^s)j9(BM6e*7IBK6Bq(!)d~zR#rbxK9NVIlgquoMq z=eGZ9NR!SEqP6=9UQg#@!rtbbSBUM#ynF);zKX+|!Zm}*{H z+j=d?aZ2!?@EL7C~%B?6ouCKLnO$uWn;Y6Xz zX8dSwj732u(o*U3F$F=7xwxm>E-B+SVZH;O-4XPuPkLSt_?S0)lb7EEg)Mglk0#eS z9@jl(OnH4juMxY+*r03VDfPx_IM!Lmc(5hOI;`?d37f>jPP$?9jQQIQU@i4vuG6MagEoJrQ=RD7xt@8E;c zeGV*+Pt+t$@pt!|McETOE$9k=_C!70uhwRS9X#b%ZK z%q(TIUXSS^F0`4Cx?Rk07C6wI4!UVPeI~-fxY6`YH$kABdOuiRtl73MqG|~AzZ@iL&^s?24iS;RK_pdlWkhcF z@Wv-Om(Aealfg)D^adlXh9Nvf~Uf@y;g3Y)i(YP zEXDnb1V}1pJT5ZWyw=1i+0fni9yINurD=EqH^ciOwLUGi)C%Da)tyt=zq2P7pV5-G zR7!oq28-Fgn5pW|nlu^b!S1Z#r7!Wtr{5J5PQ>pd+2P7RSD?>(U7-|Y z7ZQ5lhYIl_IF<9?T9^IPK<(Hp;l5bl5tF9>X-zG14_7PfsA>6<$~A338iYRT{a@r_ zuXBaT=`T5x3=s&3=RYx6NgG>No4?5KFBVjE(swfcivcIpPQFx5l+O;fiGsOrl5teR z_Cm+;PW}O0Dwe_(4Z@XZ)O0W-v2X><&L*<~*q3dg;bQW3g7)a#3KiQP>+qj|qo*Hk z?57>f2?f@`=Fj^nkDKeRkN2d$Z@2eNKpHo}ksj-$`QKb6n?*$^*%Fb3_Kbf1(*W9K>{L$mud2WHJ=j0^=g30Xhg8$#g^?36`p1fm;;1@0Lrx+8t`?vN0ZorM zSW?rhjCE8$C|@p^sXdx z|NOHHg+fL;HIlqyLp~SSdIF`TnSHehNCU9t89yr@)FY<~hu+X`tjg(aSVae$wDG*C zq$nY(Y494R)hD!i1|IIyP*&PD_c2FPgeY)&mX1qujB1VHPG9`yFQpLFVQ0>EKS@Bp zAfP5`C(sWGLI?AC{XEjLKR4FVNw(4+9b?kba95ukgR1H?w<8F7)G+6&(zUhIE5Ef% z=fFkL3QKA~M@h{nzjRq!Y_t!%U66#L8!(2-GgFxkD1=JRRqk=n%G(yHKn%^&$dW>; zSjAcjETMz1%205se$iH_)ZCpfg_LwvnsZQAUCS#^FExp8O4CrJb6>JquNV@qPq~3A zZ<6dOU#6|8+fcgiA#~MDmcpIEaUO02L5#T$HV0$EMD94HT_eXLZ2Zi&(! z&5E>%&|FZ`)CN10tM%tLSPD*~r#--K(H-CZqIOb99_;m|D5wdgJ<1iOJz@h2Zkq?} z%8_KXb&hf=2Wza(Wgc;3v3TN*;HTU*q2?#z&tLn_U0Nt!y>Oo>+2T)He6%XuP;fgn z-G!#h$Y2`9>Jtf}hbVrm6D70|ERzLAU>3zoWhJmjWfgM^))T+2u$~5>HF9jQDkrXR z=IzX36)V75PrFjkQ%TO+iqKGCQ-DDXbaE;C#}!-CoWQx&v*vHfyI>$HNRbpvm<`O( zlx9NBWD6_e&J%Ous4yp~s6)Ghni!I6)0W;9(9$y1wWu`$gs<$9Mcf$L*piP zPR0Av*2%ul`W;?-1_-5Zy0~}?`e@Y5A&0H!^ApyVTT}BiOm4GeFo$_oPlDEyeGBbh z1h3q&Dx~GmUS|3@4V36&$2uO8!Yp&^pD7J5&TN{?xphf*-js1fP?B|`>p_K>lh{ij zP(?H%e}AIP?_i^f&Li=FDSQ`2_NWxL+BB=nQr=$ zHojMlXNGauvvwPU>ZLq!`bX-5F4jBJ&So{kE5+ms9UEYD{66!|k~3vsP+mE}x!>%P za98bAU0!h0&ka4EoiDvBM#CP#dRNdXJcb*(%=<(g+M@<)DZ!@v1V>;54En?igcHR2 zhubQMq}VSOK)onqHfczM7YA@s=9*ow;k;8)&?J3@0JiGcP! zP#00KZ1t)GyZeRJ=f0^gc+58lc4Qh*S7RqPIC6GugG1gXe$LIQMRCo8cHf^qXgAa2 z`}t>u2Cq1CbSEpLr~E=c7~=Qkc9-vLE%(v9N*&HF`(d~(0`iukl5aQ9u4rUvc8%m) zr2GwZN4!s;{SB87lJB;veebPmqE}tSpT>+`t?<457Q9iV$th%i__Z1kOMAswFldD6 ztbOvO337S5o#ZZgN2G99_AVqPv!?Gmt3pzgD+Hp3QPQ`9qJ(g=kjvD+fUSS3upJn! zqoG7acIKEFRX~S}3|{EWT$kdz#zrDlJU(rPkxjws_iyLKU8+v|*oS_W*-guAb&Pj1 z35Z`3z<&Jb@2Mwz=KXucNYdY#SNO$tcVFr9KdKm|%^e-TXzs6M`PBper%ajkrIyUe zp$vVxVs9*>Vp4_1NC~Zg)WOCPmOxI1V34QlG4!aSFOH{QqSVq1^1)- z0P!Z?tT&E-ll(pwf0?=F=yOzik=@nh1Clxr9}Vij89z)ePDSCYAqw?lVI?v?+&*zH z)p$CScFI8rrwId~`}9YWPFu0cW1Sf@vRELs&cbntRU6QfPK-SO*mqu|u~}8AJ!Q$z znzu}50O=YbjwKCuSVBs6&CZR#0FTu)3{}qJJYX(>QPr4$RqWiwX3NT~;>cLn*_&1H zaKpIW)JVJ>b{uo2oq>oQt3y=zJjb%fU@wLqM{SyaC6x2snMx-}ivfU<1- znu1Lh;i$3Tf$Kh5Uk))G!D1UhE8pvx&nO~w^fG)BC&L!_hQk%^p`Kp@F{cz>80W&T ziOK=Sq3fdRu*V0=S53rcIfWFazI}Twj63CG(jOB;$*b`*#B9uEnBM`hDk*EwSRdwP8?5T?xGUKs=5N83XsR*)a4|ijz|c{4tIU+4j^A5C<#5 z*$c_d=5ml~%pGxw#?*q9N7aRwPux5EyqHVkdJO=5J>84!X6P>DS8PTTz>7C#FO?k#edkntG+fJk8ZMn?pmJSO@`x-QHq;7^h6GEXLXo1TCNhH z8ZDH{*NLAjo3WM`xeb=X{((uv3H(8&r8fJJg_uSs_%hOH%JDD?hu*2NvWGYD+j)&` zz#_1%O1wF^o5ryt?O0n;`lHbzp0wQ?rcbW(F1+h7_EZZ9{>rePvLAPVZ_R|n@;b$;UchU=0j<6k8G9QuQf@76oiE*4 zXOLQ&n3$NR#p4<5NJMVC*S);5x2)eRbaAM%VxWu9ohlT;pGEk7;002enCbQ>2r-us z3#bpXP9g|mE`65VrN`+3mC)M(eMj~~eOf)do<@l+fMiTR)XO}422*1SL{wyY(%oMpBgJagtiDf zz>O6(m;};>Hi=t8o{DVC@YigqS(Qh+ix3Rwa9aliH}a}IlOCW1@?%h_bRbq-W{KHF z%Vo?-j@{Xi@=~Lz5uZP27==UGE15|g^0gzD|3x)SCEXrx`*MP^FDLl%pOi~~Il;dc z^hrwp9sYeT7iZ)-ajKy@{a`kr0-5*_!XfBpXwEcFGJ;%kV$0Nx;apKrur zJN2J~CAv{Zjj%FolyurtW8RaFmpn&zKJWL>(0;;+q(%(Hx!GMW4AcfP0YJ*Vz!F4g z!ZhMyj$BdXL@MlF%KeInmPCt~9&A!;cRw)W!Hi@0DY(GD_f?jeV{=s=cJ6e}JktJw zQORnxxj3mBxfrH=x{`_^Z1ddDh}L#V7i}$njUFRVwOX?qOTKjfPMBO4y(WiU<)epb zvB9L=%jW#*SL|Nd_G?E*_h1^M-$PG6Pc_&QqF0O-FIOpa4)PAEPsyvB)GKasmBoEt z?_Q2~QCYGH+hW31x-B=@5_AN870vY#KB~3a*&{I=f);3Kv7q4Q7s)0)gVYx2#Iz9g(F2;=+Iy4 z6KI^8GJ6D@%tpS^8boU}zpi=+(5GfIR)35PzrbuXeL1Y1N%JK7PG|^2k3qIqHfX;G zQ}~JZ-UWx|60P5?d1e;AHx!_;#PG%d=^X(AR%i`l0jSpYOpXoKFW~7ip7|xvN;2^? zsYC9fanpO7rO=V7+KXqVc;Q5z%Bj})xHVrgoR04sA2 zl~DAwv=!(()DvH*=lyhIlU^hBkA0$e*7&fJpB0|oB7)rqGK#5##2T`@_I^|O2x4GO z;xh6ROcV<9>?e0)MI(y++$-ksV;G;Xe`lh76T#Htuia+(UrIXrf9?

L(tZ$0BqX1>24?V$S+&kLZ`AodQ4_)P#Q3*4xg8}lMV-FLwC*cN$< zt65Rf%7z41u^i=P*qO8>JqXPrinQFapR7qHAtp~&RZ85$>ob|Js;GS^y;S{XnGiBc zGa4IGvDl?x%gY`vNhv8wgZnP#UYI-w*^4YCZnxkF85@ldepk$&$#3EAhrJY0U)lR{F6sM3SONV^+$;Zx8BD&Eku3K zKNLZyBni3)pGzU0;n(X@1fX8wYGKYMpLmCu{N5-}epPDxClPFK#A@02WM3!myN%bkF z|GJ4GZ}3sL{3{qXemy+#Uk{4>Kf8v11;f8I&c76+B&AQ8udd<8gU7+BeWC`akUU~U zgXoxie>MS@rBoyY8O8Tc&8id!w+_ooxcr!1?#rc$-|SBBtH6S?)1e#P#S?jFZ8u-Bs&k`yLqW|{j+%c#A4AQ>+tj$Y z^CZajspu$F%73E68Lw5q7IVREED9r1Ijsg#@DzH>wKseye>hjsk^{n0g?3+gs@7`i zHx+-!sjLx^fS;fY!ERBU+Q zVJ!e0hJH%P)z!y%1^ZyG0>PN@5W~SV%f>}c?$H8r;Sy-ui>aruVTY=bHe}$e zi&Q4&XK!qT7-XjCrDaufT@>ieQ&4G(SShUob0Q>Gznep9fR783jGuUynAqc6$pYX; z7*O@@JW>O6lKIk0G00xsm|=*UVTQBB`u1f=6wGAj%nHK_;Aqmfa!eAykDmi-@u%6~ z;*c!pS1@V8r@IX9j&rW&d*}wpNs96O2Ute>%yt{yv>k!6zfT6pru{F1M3P z2WN1JDYqoTB#(`kE{H676QOoX`cnqHl1Yaru)>8Ky~VU{)r#{&s86Vz5X)v15ULHA zAZDb{99+s~qI6;-dQ5DBjHJP@GYTwn;Dv&9kE<0R!d z8tf1oq$kO`_sV(NHOSbMwr=To4r^X$`sBW4$gWUov|WY?xccQJN}1DOL|GEaD_!@& z15p?Pj+>7d`@LvNIu9*^hPN)pwcv|akvYYq)ks%`G>!+!pW{-iXPZsRp8 z35LR;DhseQKWYSD`%gO&k$Dj6_6q#vjWA}rZcWtQr=Xn*)kJ9kacA=esi*I<)1>w^ zO_+E>QvjP)qiSZg9M|GNeLtO2D7xT6vsj`88sd!94j^AqxFLi}@w9!Y*?nwWARE0P znuI_7A-saQ+%?MFA$gttMV-NAR^#tjl_e{R$N8t2NbOlX373>e7Ox=l=;y#;M7asp zRCz*CLnrm$esvSb5{T<$6CjY zmZ(i{Rs_<#pWW>(HPaaYj`%YqBra=Ey3R21O7vUbzOkJJO?V`4-D*u4$Me0Bx$K(lYo`JO}gnC zx`V}a7m-hLU9Xvb@K2ymioF)vj12<*^oAqRuG_4u%(ah?+go%$kOpfb`T96P+L$4> zQ#S+sA%VbH&mD1k5Ak7^^dZoC>`1L%i>ZXmooA!%GI)b+$D&ziKrb)a=-ds9xk#~& z7)3iem6I|r5+ZrTRe_W861x8JpD`DDIYZNm{$baw+$)X^Jtjnl0xlBgdnNY}x%5za zkQ8E6T<^$sKBPtL4(1zi_Rd(tVth*3Xs!ulflX+70?gb&jRTnI8l+*Aj9{|d%qLZ+ z>~V9Z;)`8-lds*Zgs~z1?Fg?Po7|FDl(Ce<*c^2=lFQ~ahwh6rqSjtM5+$GT>3WZW zj;u~w9xwAhOc<kF}~`CJ68 z?(S5vNJa;kriPlim33{N5`C{9?NWhzsna_~^|K2k4xz1`xcui*LXL-1#Y}Hi9`Oo!zQ>x-kgAX4LrPz63uZ+?uG*84@PKq-KgQlMNRwz=6Yes) zY}>YN+qP}nwr$(CZQFjUOI=-6J$2^XGvC~EZ+vrqWaOXB$k?%Suf5k=4>AveC1aJ! ziaW4IS%F$_Babi)kA8Y&u4F7E%99OPtm=vzw$$ zEz#9rvn`Iot_z-r3MtV>k)YvErZ<^Oa${`2>MYYODSr6?QZu+be-~MBjwPGdMvGd!b!elsdi4% z`37W*8+OGulab8YM?`KjJ8e+jM(tqLKSS@=jimq3)Ea2EB%88L8CaM+aG7;27b?5` z4zuUWBr)f)k2o&xg{iZ$IQkJ+SK>lpq4GEacu~eOW4yNFLU!Kgc{w4&D$4ecm0f}~ zTTzquRW@`f0}|IILl`!1P+;69g^upiPA6F{)U8)muWHzexRenBU$E^9X-uIY2%&1w z_=#5*(nmxJ9zF%styBwivi)?#KMG96-H@hD-H_&EZiRNsfk7mjBq{L%!E;Sqn!mVX*}kXhwH6eh;b42eD!*~upVG@ z#smUqz$ICm!Y8wY53gJeS|Iuard0=;k5i5Z_hSIs6tr)R4n*r*rE`>38Pw&lkv{_r!jNN=;#?WbMj|l>cU(9trCq; z%nN~r^y7!kH^GPOf3R}?dDhO=v^3BeP5hF|%4GNQYBSwz;x({21i4OQY->1G=KFyu z&6d`f2tT9Yl_Z8YACZaJ#v#-(gcyeqXMhYGXb=t>)M@fFa8tHp2x;ODX=Ap@a5I=U z0G80^$N0G4=U(>W%mrrThl0DjyQ-_I>+1Tdd_AuB3qpYAqY54upwa3}owa|x5iQ^1 zEf|iTZxKNGRpI>34EwkIQ2zHDEZ=(J@lRaOH>F|2Z%V_t56Km$PUYu^xA5#5Uj4I4RGqHD56xT%H{+P8Ag>e_3pN$4m8n>i%OyJFPNWaEnJ4McUZPa1QmOh?t8~n& z&RulPCors8wUaqMHECG=IhB(-tU2XvHP6#NrLVyKG%Ee*mQ5Ps%wW?mcnriTVRc4J`2YVM>$ixSF2Xi+Wn(RUZnV?mJ?GRdw%lhZ+t&3s7g!~g{%m&i<6 z5{ib-<==DYG93I(yhyv4jp*y3#*WNuDUf6`vTM%c&hiayf(%=x@4$kJ!W4MtYcE#1 zHM?3xw63;L%x3drtd?jot!8u3qeqctceX3m;tWetK+>~q7Be$h>n6riK(5@ujLgRS zvOym)k+VAtyV^mF)$29Y`nw&ijdg~jYpkx%*^ z8dz`C*g=I?;clyi5|!27e2AuSa$&%UyR(J3W!A=ZgHF9OuKA34I-1U~pyD!KuRkjA zbkN!?MfQOeN>DUPBxoy5IX}@vw`EEB->q!)8fRl_mqUVuRu|C@KD-;yl=yKc=ZT0% zB$fMwcC|HE*0f8+PVlWHi>M`zfsA(NQFET?LrM^pPcw`cK+Mo0%8*x8@65=CS_^$cG{GZQ#xv($7J z??R$P)nPLodI;P!IC3eEYEHh7TV@opr#*)6A-;EU2XuogHvC;;k1aI8asq7ovoP!* z?x%UoPrZjj<&&aWpsbr>J$Er-7!E(BmOyEv!-mbGQGeJm-U2J>74>o5x`1l;)+P&~ z>}f^=Rx(ZQ2bm+YE0u=ZYrAV@apyt=v1wb?R@`i_g64YyAwcOUl=C!i>=Lzb$`tjv zOO-P#A+)t-JbbotGMT}arNhJmmGl-lyUpMn=2UacVZxmiG!s!6H39@~&uVokS zG=5qWhfW-WOI9g4!R$n7!|ViL!|v3G?GN6HR0Pt_L5*>D#FEj5wM1DScz4Jv@Sxnl zB@MPPmdI{(2D?;*wd>3#tjAirmUnQoZrVv`xM3hARuJksF(Q)wd4P$88fGYOT1p6U z`AHSN!`St}}UMBT9o7i|G`r$ zrB=s$qV3d6$W9@?L!pl0lf%)xs%1ko^=QY$ty-57=55PvP(^6E7cc zGJ*>m2=;fOj?F~yBf@K@9qwX0hA803Xw+b0m}+#a(>RyR8}*Y<4b+kpp|OS+!whP( zH`v{%s>jsQI9rd$*vm)EkwOm#W_-rLTHcZRek)>AtF+~<(did)*oR1|&~1|e36d-d zgtm5cv1O0oqgWC%Et@P4Vhm}Ndl(Y#C^MD03g#PH-TFy+7!Osv1z^UWS9@%JhswEq~6kSr2DITo59+; ze=ZC}i2Q?CJ~Iyu?vn|=9iKV>4j8KbxhE4&!@SQ^dVa-gK@YfS9xT(0kpW*EDjYUkoj! zE49{7H&E}k%5(>sM4uGY)Q*&3>{aitqdNnRJkbOmD5Mp5rv-hxzOn80QsG=HJ_atI-EaP69cacR)Uvh{G5dTpYG7d zbtmRMq@Sexey)||UpnZ?;g_KMZq4IDCy5}@u!5&B^-=6yyY{}e4Hh3ee!ZWtL*s?G zxG(A!<9o!CL+q?u_utltPMk+hn?N2@?}xU0KlYg?Jco{Yf@|mSGC<(Zj^yHCvhmyx z?OxOYoxbptDK()tsJ42VzXdINAMWL$0Gcw?G(g8TMB)Khw_|v9`_ql#pRd2i*?CZl z7k1b!jQB=9-V@h%;Cnl7EKi;Y^&NhU0mWEcj8B|3L30Ku#-9389Q+(Yet0r$F=+3p z6AKOMAIi|OHyzlHZtOm73}|ntKtFaXF2Fy|M!gOh^L4^62kGUoWS1i{9gsds_GWBc zLw|TaLP64z3z9?=R2|T6Xh2W4_F*$cq>MtXMOy&=IPIJ`;!Tw?PqvI2b*U1)25^<2 zU_ZPoxg_V0tngA0J+mm?3;OYw{i2Zb4x}NedZug!>EoN3DC{1i)Z{Z4m*(y{ov2%- zk(w>+scOO}MN!exSc`TN)!B=NUX`zThWO~M*ohqq;J2hx9h9}|s#?@eR!=F{QTrq~ zTcY|>azkCe$|Q0XFUdpFT=lTcyW##i;-e{}ORB4D?t@SfqGo_cS z->?^rh$<&n9DL!CF+h?LMZRi)qju!meugvxX*&jfD!^1XB3?E?HnwHP8$;uX{Rvp# zh|)hM>XDv$ZGg=$1{+_bA~u-vXqlw6NH=nkpyWE0u}LQjF-3NhATL@9rRxMnpO%f7 z)EhZf{PF|mKIMFxnC?*78(}{Y)}iztV12}_OXffJ;ta!fcFIVjdchyHxH=t%ci`Xd zX2AUB?%?poD6Zv*&BA!6c5S#|xn~DK01#XvjT!w!;&`lDXSJT4_j$}!qSPrb37vc{ z9^NfC%QvPu@vlxaZ;mIbn-VHA6miwi8qJ~V;pTZkKqqOii<1Cs}0i?uUIss;hM4dKq^1O35y?Yp=l4i zf{M!@QHH~rJ&X~8uATV><23zZUbs-J^3}$IvV_ANLS08>k`Td7aU_S1sLsfi*C-m1 z-e#S%UGs4E!;CeBT@9}aaI)qR-6NU@kvS#0r`g&UWg?fC7|b^_HyCE!8}nyh^~o@< zpm7PDFs9yxp+byMS(JWm$NeL?DNrMCNE!I^ko-*csB+dsf4GAq{=6sfyf4wb>?v1v zmb`F*bN1KUx-`ra1+TJ37bXNP%`-Fd`vVQFTwWpX@;s(%nDQa#oWhgk#mYlY*!d>( zE&!|ySF!mIyfING+#%RDY3IBH_fW$}6~1%!G`suHub1kP@&DoAd5~7J55;5_noPI6eLf{t;@9Kf<{aO0`1WNKd?<)C-|?C?)3s z>wEq@8=I$Wc~Mt$o;g++5qR+(6wt9GI~pyrDJ%c?gPZe)owvy^J2S=+M^ z&WhIE`g;;J^xQLVeCtf7b%Dg#Z2gq9hp_%g)-%_`y*zb; zn9`f`mUPN-Ts&fFo(aNTsXPA|J!TJ{0hZp0^;MYHLOcD=r_~~^ymS8KLCSeU3;^QzJNqS z5{5rEAv#l(X?bvwxpU;2%pQftF`YFgrD1jt2^~Mt^~G>T*}A$yZc@(k9orlCGv&|1 zWWvVgiJsCAtamuAYT~nzs?TQFt<1LSEx!@e0~@yd6$b5!Zm(FpBl;(Cn>2vF?k zOm#TTjFwd2D-CyA!mqR^?#Uwm{NBemP>(pHmM}9;;8`c&+_o3#E5m)JzfwN?(f-a4 zyd%xZc^oQx3XT?vcCqCX&Qrk~nu;fxs@JUoyVoi5fqpi&bUhQ2y!Ok2pzsFR(M(|U zw3E+kH_zmTRQ9dUMZWRE%Zakiwc+lgv7Z%|YO9YxAy`y28`Aw;WU6HXBgU7fl@dnt z-fFBV)}H-gqP!1;V@Je$WcbYre|dRdp{xt!7sL3Eoa%IA`5CAA%;Wq8PktwPdULo! z8!sB}Qt8#jH9Sh}QiUtEPZ6H0b*7qEKGJ%ITZ|vH)5Q^2m<7o3#Z>AKc%z7_u`rXA zqrCy{-{8;9>dfllLu$^M5L z-hXs))h*qz%~ActwkIA(qOVBZl2v4lwbM>9l70Y`+T*elINFqt#>OaVWoja8RMsep z6Or3f=oBnA3vDbn*+HNZP?8LsH2MY)x%c13@(XfuGR}R?Nu<|07{$+Lc3$Uv^I!MQ z>6qWgd-=aG2Y^24g4{Bw9ueOR)(9h`scImD=86dD+MnSN4$6 z^U*o_mE-6Rk~Dp!ANp#5RE9n*LG(Vg`1)g6!(XtDzsov$Dvz|Gv1WU68J$CkshQhS zCrc|cdkW~UK}5NeaWj^F4MSgFM+@fJd{|LLM)}_O<{rj z+?*Lm?owq?IzC%U%9EBga~h-cJbIu=#C}XuWN>OLrc%M@Gu~kFEYUi4EC6l#PR2JS zQUkGKrrS#6H7}2l0F@S11DP`@pih0WRkRJl#F;u{c&ZC{^$Z+_*lB)r)-bPgRFE;* zl)@hK4`tEP=P=il02x7-C7p%l=B`vkYjw?YhdJU9!P!jcmY$OtC^12w?vy3<<=tlY zUwHJ_0lgWN9vf>1%WACBD{UT)1qHQSE2%z|JHvP{#INr13jM}oYv_5#xsnv9`)UAO zuwgyV4YZ;O)eSc3(mka6=aRohi!HH@I#xq7kng?Acdg7S4vDJb6cI5fw?2z%3yR+| zU5v@Hm}vy;${cBp&@D=HQ9j7NcFaOYL zj-wV=eYF{|XTkFNM2uz&T8uH~;)^Zo!=KP)EVyH6s9l1~4m}N%XzPpduPg|h-&lL` zAXspR0YMOKd2yO)eMFFJ4?sQ&!`dF&!|niH*!^*Ml##o0M(0*uK9&yzekFi$+mP9s z>W9d%Jb)PtVi&-Ha!o~Iyh@KRuKpQ@)I~L*d`{O8!kRObjO7=n+Gp36fe!66neh+7 zW*l^0tTKjLLzr`x4`_8&on?mjW-PzheTNox8Hg7Nt@*SbE-%kP2hWYmHu#Fn@Q^J(SsPUz*|EgOoZ6byg3ew88UGdZ>9B2Tq=jF72ZaR=4u%1A6Vm{O#?@dD!(#tmR;eP(Fu z{$0O%=Vmua7=Gjr8nY%>ul?w=FJ76O2js&17W_iq2*tb!i{pt#`qZB#im9Rl>?t?0c zicIC}et_4d+CpVPx)i4~$u6N-QX3H77ez z?ZdvXifFk|*F8~L(W$OWM~r`pSk5}#F?j_5u$Obu9lDWIknO^AGu+Blk7!9Sb;NjS zncZA?qtASdNtzQ>z7N871IsPAk^CC?iIL}+{K|F@BuG2>qQ;_RUYV#>hHO(HUPpk@ z(bn~4|F_jiZi}Sad;_7`#4}EmD<1EiIxa48QjUuR?rC}^HRocq`OQPM@aHVKP9E#q zy%6bmHygCpIddPjE}q_DPC`VH_2m;Eey&ZH)E6xGeStOK7H)#+9y!%-Hm|QF6w#A( zIC0Yw%9j$s-#odxG~C*^MZ?M<+&WJ+@?B_QPUyTg9DJGtQN#NIC&-XddRsf3n^AL6 zT@P|H;PvN;ZpL0iv$bRb7|J{0o!Hq+S>_NrH4@coZtBJu#g8#CbR7|#?6uxi8d+$g z87apN>EciJZ`%Zv2**_uiET9Vk{pny&My;+WfGDw4EVL#B!Wiw&M|A8f1A@ z(yFQS6jfbH{b8Z-S7D2?Ixl`j0{+ZnpT=;KzVMLW{B$`N?Gw^Fl0H6lT61%T2AU**!sX0u?|I(yoy&Xveg7XBL&+>n6jd1##6d>TxE*Vj=8lWiG$4=u{1UbAa5QD>5_ z;Te^42v7K6Mmu4IWT6Rnm>oxrl~b<~^e3vbj-GCdHLIB_>59}Ya+~OF68NiH=?}2o zP(X7EN=quQn&)fK>M&kqF|<_*H`}c zk=+x)GU>{Af#vx&s?`UKUsz})g^Pc&?Ka@t5$n$bqf6{r1>#mWx6Ep>9|A}VmWRnowVo`OyCr^fHsf# zQjQ3Ttp7y#iQY8l`zEUW)(@gGQdt(~rkxlkefskT(t%@i8=|p1Y9Dc5bc+z#n$s13 zGJk|V0+&Ekh(F};PJzQKKo+FG@KV8a<$gmNSD;7rd_nRdc%?9)p!|B-@P~kxQG}~B zi|{0}@}zKC(rlFUYp*dO1RuvPC^DQOkX4<+EwvBAC{IZQdYxoq1Za!MW7%p7gGr=j zzWnAq%)^O2$eItftC#TTSArUyL$U54-O7e|)4_7%Q^2tZ^0-d&3J1}qCzR4dWX!)4 zzIEKjgnYgMus^>6uw4Jm8ga6>GBtMjpNRJ6CP~W=37~||gMo_p@GA@#-3)+cVYnU> zE5=Y4kzl+EbEh%dhQokB{gqNDqx%5*qBusWV%!iprn$S!;oN_6E3?0+umADVs4ako z?P+t?m?};gev9JXQ#Q&KBpzkHPde_CGu-y z<{}RRAx=xlv#mVi+Ibrgx~ujW$h{?zPfhz)Kp7kmYS&_|97b&H&1;J-mzrBWAvY} zh8-I8hl_RK2+nnf&}!W0P+>5?#?7>npshe<1~&l_xqKd0_>dl_^RMRq@-Myz&|TKZBj1=Q()) zF{dBjv5)h=&Z)Aevx}+i|7=R9rG^Di!sa)sZCl&ctX4&LScQ-kMncgO(9o6W6)yd< z@Rk!vkja*X_N3H=BavGoR0@u0<}m-7|2v!0+2h~S2Q&a=lTH91OJsvms2MT~ zY=c@LO5i`mLpBd(vh|)I&^A3TQLtr>w=zoyzTd=^f@TPu&+*2MtqE$Avf>l>}V|3-8Fp2hzo3y<)hr_|NO(&oSD z!vEjTWBxbKTiShVl-U{n*B3#)3a8$`{~Pk}J@elZ=>Pqp|MQ}jrGv7KrNcjW%TN_< zZz8kG{#}XoeWf7qY?D)L)8?Q-b@Na&>i=)(@uNo zr;cH98T3$Iau8Hn*@vXi{A@YehxDE2zX~o+RY`)6-X{8~hMpc#C`|8y> zU8Mnv5A0dNCf{Ims*|l-^ z(MRp{qoGohB34|ggDI*p!Aw|MFyJ|v+<+E3brfrI)|+l3W~CQLPbnF@G0)P~Ly!1TJLp}xh8uW`Q+RB-v`MRYZ9Gam3cM%{ zb4Cb*f)0deR~wtNb*8w-LlIF>kc7DAv>T0D(a3@l`k4TFnrO+g9XH7;nYOHxjc4lq zMmaW6qpgAgy)MckYMhl?>sq;-1E)-1llUneeA!ya9KM$)DaNGu57Z5aE>=VST$#vb zFo=uRHr$0M{-ha>h(D_boS4zId;3B|Tpqo|?B?Z@I?G(?&Iei+-{9L_A9=h=Qfn-U z1wIUnQe9!z%_j$F_{rf&`ZFSott09gY~qrf@g3O=Y>vzAnXCyL!@(BqWa)Zqt!#_k zfZHuwS52|&&)aK;CHq9V-t9qt0au{$#6c*R#e5n3rje0hic7c7m{kW$p(_`wB=Gw7 z4k`1Hi;Mc@yA7dp@r~?@rfw)TkjAW++|pkfOG}0N|2guek}j8Zen(!+@7?qt_7ndX zB=BG6WJ31#F3#Vk3=aQr8T)3`{=p9nBHlKzE0I@v`{vJ}h8pd6vby&VgFhzH|q;=aonunAXL6G2y(X^CtAhWr*jI zGjpY@raZDQkg*aMq}Ni6cRF z{oWv}5`nhSAv>usX}m^GHt`f(t8@zHc?K|y5Zi=4G*UG1Sza{$Dpj%X8 zzEXaKT5N6F5j4J|w#qlZP!zS7BT)9b+!ZSJdToqJts1c!)fwih4d31vfb{}W)EgcA zH2pZ^8_k$9+WD2n`6q5XbOy8>3pcYH9 z07eUB+p}YD@AH!}p!iKv><2QF-Y^&xx^PAc1F13A{nUeCDg&{hnix#FiO!fe(^&%Qcux!h znu*S!s$&nnkeotYsDthh1dq(iQrE|#f_=xVgfiiL&-5eAcC-> z5L0l|DVEM$#ulf{bj+Y~7iD)j<~O8CYM8GW)dQGq)!mck)FqoL^X zwNdZb3->hFrbHFm?hLvut-*uK?zXn3q1z|UX{RZ;-WiLoOjnle!xs+W0-8D)kjU#R z+S|A^HkRg$Ij%N4v~k`jyHffKaC~=wg=9)V5h=|kLQ@;^W!o2^K+xG&2n`XCd>OY5Ydi= zgHH=lgy++erK8&+YeTl7VNyVm9-GfONlSlVb3)V9NW5tT!cJ8d7X)!b-$fb!s76{t z@d=Vg-5K_sqHA@Zx-L_}wVnc@L@GL9_K~Zl(h5@AR#FAiKad8~KeWCo@mgXIQ#~u{ zgYFwNz}2b6Vu@CP0XoqJ+dm8px(5W5-Jpis97F`+KM)TuP*X8H@zwiVKDKGVp59pI zifNHZr|B+PG|7|Y<*tqap0CvG7tbR1R>jn70t1X`XJixiMVcHf%Ez*=xm1(CrTSDt z0cle!+{8*Ja&EOZ4@$qhBuKQ$U95Q%rc7tg$VRhk?3=pE&n+T3upZg^ZJc9~c2es% zh7>+|mrmA-p&v}|OtxqmHIBgUxL~^0+cpfkSK2mhh+4b=^F1Xgd2)}U*Yp+H?ls#z zrLxWg_hm}AfK2XYWr!rzW4g;+^^&bW%LmbtRai9f3PjU${r@n`JThy-cphbcwn)rq9{A$Ht`lmYKxOacy z6v2R(?gHhD5@&kB-Eg?4!hAoD7~(h>(R!s1c1Hx#s9vGPePUR|of32bS`J5U5w{F) z>0<^ktO2UHg<0{oxkdOQ;}coZDQph8p6ruj*_?uqURCMTac;>T#v+l1Tc~%^k-Vd@ zkc5y35jVNc49vZpZx;gG$h{%yslDI%Lqga1&&;mN{Ush1c7p>7e-(zp}6E7f-XmJb4nhk zb8zS+{IVbL$QVF8pf8}~kQ|dHJAEATmmnrb_wLG}-yHe>W|A&Y|;muy-d^t^<&)g5SJfaTH@P1%euONny=mxo+C z4N&w#biWY41r8k~468tvuYVh&XN&d#%QtIf9;iVXfWY)#j=l`&B~lqDT@28+Y!0E+MkfC}}H*#(WKKdJJq=O$vNYCb(ZG@p{fJgu;h z21oHQ(14?LeT>n5)s;uD@5&ohU!@wX8w*lB6i@GEH0pM>YTG+RAIWZD;4#F1&F%Jp zXZUml2sH0!lYJT?&sA!qwez6cXzJEd(1ZC~kT5kZSp7(@=H2$Azb_*W&6aA|9iwCL zdX7Q=42;@dspHDwYE?miGX#L^3xD&%BI&fN9^;`v4OjQXPBaBmOF1;#C)8XA(WFlH zycro;DS2?(G&6wkr6rqC>rqDv3nfGw3hmN_9Al>TgvmGsL8_hXx09};l9Ow@)F5@y z#VH5WigLDwZE4nh^7&@g{1FV^UZ%_LJ-s<{HN*2R$OPg@R~Z`c-ET*2}XB@9xvAjrK&hS=f|R8Gr9 zr|0TGOsI7RD+4+2{ZiwdVD@2zmg~g@^D--YL;6UYGSM8i$NbQr4!c7T9rg!8;TM0E zT#@?&S=t>GQm)*ua|?TLT2ktj#`|R<_*FAkOu2Pz$wEc%-=Y9V*$&dg+wIei3b*O8 z2|m$!jJG!J!ZGbbIa!(Af~oSyZV+~M1qGvelMzPNE_%5?c2>;MeeG2^N?JDKjFYCy z7SbPWH-$cWF9~fX%9~v99L!G(wi!PFp>rB!9xj7=Cv|F+7CsGNwY0Q_J%FID%C^CBZQfJ9K(HK%k31j~e#&?hQ zNuD6gRkVckU)v+53-fc} z7ZCzYN-5RG4H7;>>Hg?LU9&5_aua?A0)0dpew1#MMlu)LHe(M;OHjHIUl7|%%)YPo z0cBk;AOY00%Fe6heoN*$(b<)Cd#^8Iu;-2v@>cE-OB$icUF9EEoaC&q8z9}jMTT2I z8`9;jT%z0;dy4!8U;GW{i`)3!c6&oWY`J3669C!tM<5nQFFrFRglU8f)5Op$GtR-3 zn!+SPCw|04sv?%YZ(a7#L?vsdr7ss@WKAw&A*}-1S|9~cL%uA+E~>N6QklFE>8W|% zyX-qAUGTY1hQ-+um`2|&ji0cY*(qN!zp{YpDO-r>jPk*yuVSay<)cUt`t@&FPF_&$ zcHwu1(SQ`I-l8~vYyUxm@D1UEdFJ$f5Sw^HPH7b!9 zzYT3gKMF((N(v0#4f_jPfVZ=ApN^jQJe-X$`A?X+vWjLn_%31KXE*}5_}d8 zw_B1+a#6T1?>M{ronLbHIlEsMf93muJ7AH5h%;i99<~JX^;EAgEB1uHralD*!aJ@F zV2ruuFe9i2Q1C?^^kmVy921eb=tLDD43@-AgL^rQ3IO9%+vi_&R2^dpr}x{bCVPej z7G0-0o64uyWNtr*loIvslyo0%)KSDDKjfThe0hcqs)(C-MH1>bNGBDRTW~scy_{w} zp^aq8Qb!h9Lwielq%C1b8=?Z=&U)ST&PHbS)8Xzjh2DF?d{iAv)Eh)wsUnf>UtXN( zL7=$%YrZ#|^c{MYmhn!zV#t*(jdmYdCpwqpZ{v&L8KIuKn`@IIZfp!uo}c;7J57N` zAxyZ-uA4=Gzl~Ovycz%MW9ZL7N+nRo&1cfNn9(1H5eM;V_4Z_qVann7F>5f>%{rf= zPBZFaV@_Sobl?Fy&KXyzFDV*FIdhS5`Uc~S^Gjo)aiTHgn#<0C=9o-a-}@}xDor;D zZyZ|fvf;+=3MZd>SR1F^F`RJEZo+|MdyJYQAEauKu%WDol~ayrGU3zzbHKsnHKZ*z zFiwUkL@DZ>!*x05ql&EBq@_Vqv83&?@~q5?lVmffQZ+V-=qL+!u4Xs2Z2zdCQ3U7B&QR9_Iggy} z(om{Y9eU;IPe`+p1ifLx-XWh?wI)xU9ik+m#g&pGdB5Bi<`PR*?92lE0+TkRuXI)z z5LP!N2+tTc%cB6B1F-!fj#}>S!vnpgVU~3!*U1ej^)vjUH4s-bd^%B=ItQqDCGbrEzNQi(dJ`J}-U=2{7-d zK8k^Rlq2N#0G?9&1?HSle2vlkj^KWSBYTwx`2?9TU_DX#J+f+qLiZCqY1TXHFxXZqYMuD@RU$TgcnCC{_(vwZ-*uX)~go#%PK z@}2Km_5aQ~(<3cXeJN6|F8X_1@L%@xTzs}$_*E|a^_URF_qcF;Pfhoe?FTFwvjm1o z8onf@OY@jC2tVcMaZS;|T!Ks(wOgPpRzRnFS-^RZ4E!9dsnj9sFt609a|jJbb1Dt@ z<=Gal2jDEupxUSwWu6zp<<&RnAA;d&4gKVG0iu6g(DsST(4)z6R)zDpfaQ}v{5ARt zyhwvMtF%b-YazR5XLz+oh=mn;y-Mf2a8>7?2v8qX;19y?b>Z5laGHvzH;Nu9S`B8} zI)qN$GbXIQ1VL3lnof^6TS~rvPVg4V?Dl2Bb*K2z4E{5vy<(@@K_cN@U>R!>aUIRnb zL*)=787*cs#zb31zBC49x$`=fkQbMAef)L2$dR{)6BAz!t5U_B#1zZG`^neKSS22oJ#5B=gl%U=WeqL9REF2g zZnfCb0?quf?Ztj$VXvDSWoK`0L=Zxem2q}!XWLoT-kYMOx)!7fcgT35uC~0pySEme z`{wGWTkGr7>+Kb^n;W?BZH6ZP(9tQX%-7zF>vc2}LuWDI(9kh1G#7B99r4x6;_-V+k&c{nPUrR zAXJGRiMe~aup{0qzmLNjS_BC4cB#sXjckx{%_c&^xy{M61xEb>KW_AG5VFXUOjAG4 z^>Qlm9A#1N{4snY=(AmWzatb!ngqiqPbBZ7>Uhb3)dTkSGcL#&SH>iMO-IJBPua`u zo)LWZ>=NZLr758j{%(|uQuZ)pXq_4c!!>s|aDM9#`~1bzK3J1^^D#<2bNCccH7~-X}Ggi!pIIF>uFx%aPARGQsnC8ZQc8lrQ5o~smqOg>Ti^GNme94*w z)JZy{_{#$jxGQ&`M z!OMvZMHR>8*^>eS%o*6hJwn!l8VOOjZQJvh)@tnHVW&*GYPuxqXw}%M!(f-SQf`=L z5;=5w2;%82VMH6Xi&-K3W)o&K^+vJCepWZ-rW%+Dc6X3(){z$@4zjYxQ|}8UIojeC zYZpQ1dU{fy=oTr<4VX?$q)LP}IUmpiez^O&N3E_qPpchGTi5ZM6-2ScWlQq%V&R2Euz zO|Q0Hx>lY1Q1cW5xHv5!0OGU~PVEqSuy#fD72d#O`N!C;o=m+YioGu-wH2k6!t<~K zSr`E=W9)!g==~x9VV~-8{4ZN9{~-A9zJpRe%NGg$+MDuI-dH|b@BD)~>pPCGUNNzY zMDg||0@XGQgw`YCt5C&A{_+J}mvV9Wg{6V%2n#YSRN{AP#PY?1FF1#|vO_%e+#`|2*~wGAJaeRX6=IzFNeWhz6gJc8+(03Ph4y6ELAm=AkN7TOgMUEw*N{= z_)EIDQx5q22oUR+_b*tazu9+pX|n1c*IB-}{DqIj z-?E|ks{o3AGRNb;+iKcHkZvYJvFsW&83RAPs1Oh@IWy%l#5x2oUP6ZCtv+b|q>jsf zZ_9XO;V!>n`UxH1LvH8)L4?8raIvasEhkpQoJ`%!5rBs!0Tu(s_D{`4opB;57)pkX z4$A^8CsD3U5*!|bHIEqsn~{q+Ddj$ME@Gq4JXtgVz&7l{Ok!@?EA{B3P~NAqb9)4? zkQo30A^EbHfQ@87G5&EQTd`frrwL)&Yw?%-W@uy^Gn23%j?Y!Iea2xw<-f;esq zf%w5WN@E1}zyXtYv}}`U^B>W`>XPmdLj%4{P298|SisrE;7HvXX;A}Ffi8B#3Lr;1 zHt6zVb`8{#+e$*k?w8|O{Uh|&AG}|DG1PFo1i?Y*cQm$ZwtGcVgMwtBUDa{~L1KT-{jET4w60>{KZ27vXrHJ;fW{6| z=|Y4!&UX020wU1>1iRgB@Q#m~1^Z^9CG1LqDhYBrnx%IEdIty z!46iOoKlKs)c}newDG)rWUikD%j`)p z_w9Ph&e40=(2eBy;T!}*1p1f1SAUDP9iWy^u^Ubdj21Kn{46;GR+hwLO=4D11@c~V zI8x&(D({K~Df2E)Nx_yQvYfh4;MbMJ@Z}=Dt3_>iim~QZ*hZIlEs0mEb z_54+&*?wMD`2#vsQRN3KvoT>hWofI_Vf(^C1ff-Ike@h@saEf7g}<9T`W;HAne-Nd z>RR+&SP35w)xKn8^U$7))PsM!jKwYZ*RzEcG-OlTrX3}9a{q%#Un5E5W{{hp>w~;` zGky+3(vJvQyGwBo`tCpmo0mo((?nM8vf9aXrrY1Ve}~TuVkB(zeds^jEfI}xGBCM2 zL1|#tycSaWCurP+0MiActG3LCas@_@tao@(R1ANlwB$4K53egNE_;!&(%@Qo$>h`^1S_!hN6 z)vZtG$8fN!|BXBJ=SI>e(LAU(y(i*PHvgQ2llulxS8>qsimv7yL}0q_E5WiAz7)(f zC(ahFvG8&HN9+6^jGyLHM~$)7auppeWh_^zKk&C_MQ~8;N??OlyH~azgz5fe^>~7F zl3HnPN3z-kN)I$4@`CLCMQx3sG~V8hPS^}XDXZrQA>}mQPw%7&!sd(Pp^P=tgp-s^ zjl}1-KRPNWXgV_K^HkP__SR`S-|OF0bR-N5>I%ODj&1JUeAQ3$9i;B~$S6}*^tK?= z**%aCiH7y?xdY?{LgVP}S0HOh%0%LI$wRx;$T|~Y8R)Vdwa}kGWv8?SJVm^>r6+%I z#lj1aR94{@MP;t-scEYQWc#xFA30^}?|BeX*W#9OL;Q9#WqaaM546j5j29((^_8Nu z4uq}ESLr~r*O7E7$D{!k9W>`!SLoyA53i9QwRB{!pHe8um|aDE`Cg0O*{jmor)^t)3`>V>SWN-2VJcFmj^1?~tT=JrP`fVh*t zXHarp=8HEcR#vFe+1a%XXuK+)oFs`GDD}#Z+TJ}Ri`FvKO@ek2ayn}yaOi%(8p%2$ zpEu)v0Jym@f}U|-;}CbR=9{#<^z28PzkkTNvyKvJDZe+^VS2bES3N@Jq!-*}{oQlz z@8bgC_KnDnT4}d#&Cpr!%Yb?E!brx0!eVOw~;lLwUoz#Np%d$o%9scc3&zPm`%G((Le|6o1 zM(VhOw)!f84zG^)tZ1?Egv)d8cdNi+T${=5kV+j;Wf%2{3g@FHp^Gf*qO0q!u$=m9 zCaY`4mRqJ;FTH5`a$affE5dJrk~k`HTP_7nGTY@B9o9vvnbytaID;^b=Tzp7Q#DmD zC(XEN)Ktn39z5|G!wsVNnHi) z%^q94!lL|hF`IijA^9NR0F$@h7k5R^ljOW(;Td9grRN0Mb)l_l7##{2nPQ@?;VjXv zaLZG}yuf$r$<79rVPpXg?6iiieX|r#&`p#Con2i%S8*8F}(E) zI5E6c3tG*<;m~6>!&H!GJ6zEuhH7mkAzovdhLy;)q z{H2*8I^Pb}xC4s^6Y}6bJvMu=8>g&I)7!N!5QG$xseeU#CC?ZM-TbjsHwHgDGrsD= z{%f;@Sod+Ch66Ko2WF~;Ty)v>&x^aovCbCbD7>qF*!?BXmOV3(s|nxsb*Lx_2lpB7 zokUnzrk;P=T-&kUHO}td+Zdj!3n&NR?K~cRU zAXU!DCp?51{J4w^`cV#ye}(`SQhGQkkMu}O3M*BWt4UsC^jCFUy;wTINYmhD$AT;4 z?Xd{HaJjP`raZ39qAm;%beDbrLpbRf(mkKbANan7XsL>_pE2oo^$TgdidjRP!5-`% zv0d!|iKN$c0(T|L0C~XD0aS8t{*&#LnhE;1Kb<9&=c2B+9JeLvJr*AyyRh%@jHej=AetOMSlz^=!kxX>>B{2B1uIrQyfd8KjJ+DBy!h)~*(!|&L4^Q_07SQ~E zcemVP`{9CwFvPFu7pyVGCLhH?LhEVb2{7U+Z_>o25#+3<|8%1T^5dh}*4(kfJGry} zm%r#hU+__Z;;*4fMrX=Bkc@7|v^*B;HAl0((IBPPii%X9+u3DDF6%bI&6?Eu$8&aWVqHIM7mK6?Uvq$1|(-T|)IV<>e?!(rY zqkmO1MRaLeTR=)io(0GVtQT@s6rN%C6;nS3@eu;P#ry4q;^O@1ZKCJyp_Jo)Ty^QW z+vweTx_DLm{P-XSBj~Sl<%_b^$=}odJ!S2wAcxenmzFGX1t&Qp8Vxz2VT`uQsQYtdn&_0xVivIcxZ_hnrRtwq4cZSj1c-SG9 z7vHBCA=fd0O1<4*=lu$6pn~_pVKyL@ztw1swbZi0B?spLo56ZKu5;7ZeUml1Ws1?u zqMf1p{5myAzeX$lAi{jIUqo1g4!zWLMm9cfWcnw`k6*BR^?$2(&yW?>w;G$EmTA@a z6?y#K$C~ZT8+v{87n5Dm&H6Pb_EQ@V0IWmG9cG=O;(;5aMWWrIPzz4Q`mhK;qQp~a z+BbQrEQ+w{SeiuG-~Po5f=^EvlouB@_|4xQXH@A~KgpFHrwu%dwuCR)=B&C(y6J4J zvoGk9;lLs9%iA-IJGU#RgnZZR+@{5lYl8(e1h6&>Vc_mvg0d@);X zji4T|n#lB!>pfL|8tQYkw?U2bD`W{na&;*|znjmalA&f;*U++_aBYerq;&C8Kw7mI z7tsG*?7*5j&dU)Lje;^{D_h`%(dK|pB*A*1(Jj)w^mZ9HB|vGLkF1GEFhu&rH=r=8 zMxO42e{Si6$m+Zj`_mXb&w5Q(i|Yxyg?juUrY}78uo@~3v84|8dfgbPd0iQJRdMj< zncCNGdMEcsxu#o#B5+XD{tsg*;j-eF8`mp~K8O1J!Z0+>0=7O=4M}E?)H)ENE;P*F z$Ox?ril_^p0g7xhDUf(q652l|562VFlC8^r8?lQv;TMvn+*8I}&+hIQYh2 z1}uQQaag&!-+DZ@|C+C$bN6W;S-Z@)d1|en+XGvjbOxCa-qAF*LA=6s(Jg+g;82f$ z(Vb)8I)AH@cdjGFAR5Rqd0wiNCu!xtqWbcTx&5kslzTb^7A78~Xzw1($UV6S^VWiP zFd{Rimd-0CZC_Bu(WxBFW7+k{cOW7DxBBkJdJ;VsJ4Z@lERQr%3eVv&$%)b%<~ zCl^Y4NgO}js@u{|o~KTgH}>!* z_iDNqX2(As7T0xivMH|3SC1ivm8Q}6Ffcd7owUKN5lHAtzMM4<0v+ykUT!QiowO;`@%JGv+K$bBx@*S7C8GJVqQ_K>12}M`f_Ys=S zKFh}HM9#6Izb$Y{wYzItTy+l5U2oL%boCJn?R3?jP@n$zSIwlmyGq30Cw4QBO|14` zW5c);AN*J3&eMFAk$SR~2k|&+&Bc$e>s%c{`?d~85S-UWjA>DS5+;UKZ}5oVa5O(N zqqc@>)nee)+4MUjH?FGv%hm2{IlIF-QX}ym-7ok4Z9{V+ZHVZQl$A*x!(q%<2~iVv znUa+BX35&lCb#9VE-~Y^W_f;Xhl%vgjwdjzMy$FsSIj&ok}L+X`4>J=9BkN&nu^E*gbhj3(+D>C4E z@Fwq_=N)^bKFSHTzZk?-gNU$@l}r}dwGyh_fNi=9b|n}J>&;G!lzilbWF4B}BBq4f zYIOl?b)PSh#XTPp4IS5ZR_2C!E)Z`zH0OW%4;&~z7UAyA-X|sh9@~>cQW^COA9hV4 zXcA6qUo9P{bW1_2`eo6%hgbN%(G-F1xTvq!sc?4wN6Q4`e9Hku zFwvlAcRY?6h^Fj$R8zCNEDq8`=uZB8D-xn)tA<^bFFy}4$vA}Xq0jAsv1&5!h!yRA zU()KLJya5MQ`q&LKdH#fwq&(bNFS{sKlEh_{N%{XCGO+po#(+WCLmKW6&5iOHny>g z3*VFN?mx!16V5{zyuMWDVP8U*|BGT$(%IO|)?EF|OI*sq&RovH!N%=>i_c?K*A>>k zyg1+~++zY4Q)J;VWN0axhoIKx;l&G$gvj(#go^pZskEVj8^}is3Jw26LzYYVos0HX zRPvmK$dVxM8(Tc?pHFe0Z3uq){{#OK3i-ra#@+;*=ui8)y6hsRv z4Fxx1c1+fr!VI{L3DFMwXKrfl#Q8hfP@ajgEau&QMCxd{g#!T^;ATXW)nUg&$-n25 zruy3V!!;{?OTobo|0GAxe`Acn3GV@W=&n;~&9 zQM>NWW~R@OYORkJAo+eq1!4vzmf9K%plR4(tB@TR&FSbDoRgJ8qVcH#;7lQub*nq&?Z>7WM=oeEVjkaG zT#f)=o!M2DO5hLR+op>t0CixJCIeXH*+z{-XS|%jx)y(j&}Wo|3!l7{o)HU3m7LYyhv*xF&tq z%IN7N;D4raue&&hm0xM=`qv`+TK@;_xAcGKuK(2|75~ar2Yw)geNLSmVxV@x89bQu zpViVKKnlkwjS&&c|-X6`~xdnh}Ps)Hs z4VbUL^{XNLf7_|Oi>tA%?SG5zax}esF*FH3d(JH^Gvr7Rp*n=t7frH!U;!y1gJB^i zY_M$KL_}mW&XKaDEi9K-wZR|q*L32&m+2n_8lq$xRznJ7p8}V>w+d@?uB!eS3#u<} zIaqi!b!w}a2;_BfUUhGMy#4dPx>)_>yZ`ai?Rk`}d0>~ce-PfY-b?Csd(28yX22L% zI7XI>OjIHYTk_@Xk;Gu^F52^Gn6E1&+?4MxDS2G_#PQ&yXPXP^<-p|2nLTb@AAQEY zI*UQ9Pmm{Kat}wuazpjSyXCdnrD&|C1c5DIb1TnzF}f4KIV6D)CJ!?&l&{T)e4U%3HTSYqsQ zo@zWB1o}ceQSV)<4G<)jM|@@YpL+XHuWsr5AYh^Q{K=wSV99D~4RRU52FufmMBMmd z_H}L#qe(}|I9ZyPRD6kT>Ivj&2Y?qVZq<4bG_co_DP`sE*_Xw8D;+7QR$Uq(rr+u> z8bHUWbV19i#)@@G4bCco@Xb<8u~wVDz9S`#k@ciJtlu@uP1U0X?yov8v9U3VOig2t zL9?n$P3=1U_Emi$#slR>N5wH-=J&T=EdUHA}_Z zZIl3nvMP*AZS9{cDqFanrA~S5BqxtNm9tlu;^`)3X&V4tMAkJ4gEIPl= zoV!Gyx0N{3DpD@)pv^iS*dl2FwANu;1;%EDl}JQ7MbxLMAp>)UwNwe{=V}O-5C*>F zu?Ny+F64jZn<+fKjF01}8h5H_3pey|;%bI;SFg$w8;IC<8l|3#Lz2;mNNik6sVTG3 z+Su^rIE#40C4a-587$U~%KedEEw1%r6wdvoMwpmlXH$xPnNQN#f%Z7|p)nC>WsuO= z4zyqapLS<8(UJ~Qi9d|dQijb_xhA2)v>la)<1md5s^R1N&PiuA$^k|A<+2C?OiHbj z>Bn$~t)>Y(Zb`8hW7q9xQ=s>Rv81V+UiuZJc<23HplI88isqRCId89fb`Kt|CxVIg znWcwprwXnotO>3s&Oypkte^9yJjlUVVxSe%_xlzmje|mYOVPH^vjA=?6xd0vaj0Oz zwJ4OJNiFdnHJX3rw&inskjryukl`*fRQ#SMod5J|KroJRsVXa5_$q7whSQ{gOi*s0 z1LeCy|JBWRsDPn7jCb4s(p|JZiZ8+*ExC@Vj)MF|*Vp{B(ziccSn`G1Br9bV(v!C2 z6#?eqpJBc9o@lJ#^p-`-=`4i&wFe>2)nlPK1p9yPFzJCzBQbpkcR>={YtamIw)3nt z(QEF;+)4`>8^_LU)_Q3 zC5_7lgi_6y>U%m)m@}Ku4C}=l^J=<<7c;99ec3p{aR+v=diuJR7uZi%aQv$oP?dn?@6Yu_+*^>T0ptf(oobdL;6)N-I!TO`zg^Xbv3#L0I~sn@WGk-^SmPh5>W+LB<+1PU}AKa?FCWF|qMNELOgdxR{ zbqE7@jVe+FklzdcD$!(A$&}}H*HQFTJ+AOrJYnhh}Yvta(B zQ_bW4Rr;R~&6PAKwgLWXS{Bnln(vUI+~g#kl{r+_zbngT`Y3`^Qf=!PxN4IYX#iW4 zucW7@LLJA9Zh3(rj~&SyN_pjO8H&)|(v%!BnMWySBJV=eSkB3YSTCyIeJ{i;(oc%_hk{$_l;v>nWSB)oVeg+blh=HB5JSlG_r7@P z3q;aFoZjD_qS@zygYqCn=;Zxjo!?NK!%J$ z52lOP`8G3feEj+HTp@Tnn9X~nG=;tS+z}u{mQX_J0kxtr)O30YD%oo)L@wy`jpQYM z@M>Me=95k1p*FW~rHiV1CIfVc{K8r|#Kt(ApkXKsDG$_>76UGNhHExFCw#Ky9*B-z zNq2ga*xax!HMf_|Vp-86r{;~YgQKqu7%szk8$hpvi_2I`OVbG1doP(`gn}=W<8%Gn z%81#&WjkH4GV;4u43EtSW>K_Ta3Zj!XF?;SO3V#q=<=>Tc^@?A`i;&`-cYj|;^ zEo#Jl5zSr~_V-4}y8pnufXLa80vZY4z2ko7fj>DR)#z=wWuS1$$W!L?(y}YC+yQ|G z@L&`2upy3f>~*IquAjkVNU>}c10(fq#HdbK$~Q3l6|=@-eBbo>B9(6xV`*)sae58*f zym~RRVx;xoCG3`JV`xo z!lFw)=t2Hy)e!IFs?0~7osWk(d%^wxq&>_XD4+U#y&-VF%4z?XH^i4w`TxpF{`XhZ z%G}iEzf!T(l>g;W9<~K+)$g!{UvhW{E0Lis(S^%I8OF&%kr!gJ&fMOpM=&=Aj@wuL zBX?*6i51Qb$uhkwkFYkaD_UDE+)rh1c;(&Y=B$3)J&iJfQSx!1NGgPtK!$c9OtJuu zX(pV$bfuJpRR|K(dp@^j}i&HeJOh@|7lWo8^$*o~Xqo z5Sb+!EtJ&e@6F+h&+_1ETbg7LfP5GZjvIUIN3ibCOldAv z)>YdO|NH$x7AC8dr=<2ekiY1%fN*r~e5h6Yaw<{XIErujKV~tiyrvV_DV0AzEknC- zR^xKM3i<1UkvqBj3C{wDvytOd+YtDSGu!gEMg+!&|8BQrT*|p)(dwQLEy+ zMtMzij3zo40)CA!BKZF~yWg?#lWhqD3@qR)gh~D{uZaJO;{OWV8XZ_)J@r3=)T|kt zUS1pXr6-`!Z}w2QR7nP%d?ecf90;K_7C3d!UZ`N(TZoWNN^Q~RjVhQG{Y<%E1PpV^4 z-m-K+$A~-+VDABs^Q@U*)YvhY4Znn2^w>732H?NRK(5QSS$V@D7yz2BVX4)f5A04~$WbxGOam22>t&uD)JB8-~yiQW6ik;FGblY_I>SvB_z2?PS z*Qm&qbKI{H1V@YGWzpx`!v)WeLT02};JJo*#f$a*FH?IIad-^(;9XC#YTWN6;Z6+S zm4O1KH=#V@FJw7Pha0!9Vb%ZIM$)a`VRMoiN&C|$YA3~ZC*8ayZRY^fyuP6$n%2IU z$#XceYZeqLTXw(m$_z|33I$B4k~NZO>pP6)H_}R{E$i%USGy{l{-jOE;%CloYPEU+ zRFxOn4;7lIOh!7abb23YKD+_-?O z0FP9otcAh+oSj;=f#$&*ExUHpd&e#bSF%#8*&ItcL2H$Sa)?pt0Xtf+t)z$_u^wZi z44oE}r4kIZGy3!Mc8q$B&6JqtnHZ>Znn!Zh@6rgIu|yU+zG8q`q9%B18|T|oN3zMq z`l&D;U!OL~%>vo&q0>Y==~zLiCZk4v%s_7!9DxQ~id1LLE93gf*gg&2$|hB#j8;?3 z5v4S;oM6rT{Y;I+#FdmNw z){d%tNM<<#GN%n9ox7B=3#;u7unZ~tLB_vRZ52a&2=IM)2VkXm=L+Iqq~uk#Dug|x z>S84e+A7EiOY5lj*!q?6HDkNh~0g;0Jy(al!ZHHDtur9T$y-~)94HelX1NHjXWIM7UAe}$?jiz z9?P4`I0JM=G5K{3_%2jPLC^_Mlw?-kYYgb7`qGa3@dn|^1fRMwiyM@Ch z;CB&o7&&?c5e>h`IM;Wnha0QKnEp=$hA8TJgR-07N~U5(>9vJzeoFsSRBkDq=x(YgEMpb=l4TDD`2 zwVJpWGTA_u7}?ecW7s6%rUs&NXD3+n;jB86`X?8(l3MBo6)PdakI6V6a}22{)8ilT zM~T*mU}__xSy|6XSrJ^%lDAR3Lft%+yxC|ZUvSO_nqMX!_ul3;R#*{~4DA=h$bP)%8Yv9X zyp><|e8=_ttI}ZAwOd#dlnSjck#6%273{E$kJuCGu=I@O)&6ID{nWF5@gLb16sj|&Sb~+du4e4O_%_o`Ix4NRrAsyr1_}MuP94s>de8cH-OUkVPk3+K z&jW)It9QiU-ti~AuJkL`XMca8Oh4$SyJ=`-5WU<{cIh+XVH#e4d&zive_UHC!pN>W z3TB;Mn5i)9Qn)#6@lo4QpI3jFYc0~+jS)4AFz8fVC;lD^+idw^S~Qhq>Tg(!3$yLD zzktzoFrU@6s4wwCMz}edpF5i5Q1IMmEJQHzp(LAt)pgN3&O!&d?3W@6U4)I^2V{;- z6A(?zd93hS*uQmnh4T)nHnE{wVhh(=MMD(h(P4+^p83Om6t<*cUW>l(qJzr%5vp@K zN27ka(L{JX=1~e2^)F^i=TYj&;<7jyUUR2Bek^A8+3Up*&Xwc{)1nRR5CT8vG>ExV zHnF3UqXJOAno_?bnhCX-&kwI~Ti8t4`n0%Up>!U`ZvK^w2+0Cs-b9%w%4`$+To|k= zKtgc&l}P`*8IS>8DOe?EB84^kx4BQp3<7P{Pq}&p%xF_81pg!l2|u=&I{AuUgmF5n zJQCTLv}%}xbFGYtKfbba{CBo)lWW%Z>i(_NvLhoQZ*5-@2l&x>e+I~0Nld3UI9tdL zRzu8}i;X!h8LHVvN?C+|M81e>Jr38%&*9LYQec9Ax>?NN+9(_>XSRv&6hlCYB`>Qm z1&ygi{Y()OU4@D_jd_-7vDILR{>o|7-k)Sjdxkjgvi{@S>6GqiF|o`*Otr;P)kLHN zZkpts;0zw_6;?f(@4S1FN=m!4^mv~W+lJA`&7RH%2$)49z0A+8@0BCHtj|yH--AEL z0tW6G%X-+J+5a{5*WKaM0QDznf;V?L5&uQw+yegDNDP`hA;0XPYc6e0;Xv6|i|^F2WB)Z$LR|HR4 zTQsRAby9(^Z@yATyOgcfQw7cKyr^3Tz7lc7+JEwwzA7)|2x+PtEb>nD(tpxJQm)Kn zW9K_*r!L%~N*vS8<5T=iv|o!zTe9k_2jC_j*7ik^M_ zaf%k{WX{-;0*`t`G!&`eW;gChVXnJ-Rn)To8vW-?>>a%QU1v`ZC=U)f8iA@%JG0mZ zDqH;~mgBnrCP~1II<=V9;EBL)J+xzCoiRBaeH&J6rL!{4zIY8tZka?_FBeQeNO3q6 zyG_alW54Ba&wQf{&F1v-r1R6ID)PTsqjIBc+5MHkcW5Fnvi~{-FjKe)t1bl}Y;z@< z=!%zvpRua>>t_x}^}z0<7MI!H2v6|XAyR9!t50q-A)xk0nflgF4*OQlCGK==4S|wc zRMsSscNhRzHMBU8TdcHN!q^I}x0iXJ%uehac|Zs_B$p@CnF)HeXPpB_Za}F{<@6-4 zl%kml@}kHQ(ypD8FsPJ2=14xXJE|b20RUIgs!2|R3>LUMGF6X*B_I|$`Qg=;zm7C z{mEDy9dTmPbued7mlO@phdmAmJ7p@GR1bjCkMw6*G7#4+`k>fk1czdJUB!e@Q(~6# zwo%@p@V5RL0ABU2LH7Asq^quDUho@H>eTZH9f*no9fY0T zD_-9px3e}A!>>kv5wk91%C9R1J_Nh!*&Kk$J3KNxC}c_@zlgpJZ+5L)Nw|^p=2ue}CJtm;uj*Iqr)K})kA$xtNUEvX;4!Px*^&9T_`IN{D z{6~QY=Nau6EzpvufB^hflc#XIsSq0Y9(nf$d~6ZwK}fal92)fr%T3=q{0mP-EyP_G z)UR5h@IX}3Qll2b0oCAcBF>b*@Etu*aTLPU<%C>KoOrk=x?pN!#f_Og-w+;xbFgjQ zXp`et%lDBBh~OcFnMKMUoox0YwBNy`N0q~bSPh@+enQ=4RUw1) zpovN`QoV>vZ#5LvC;cl|6jPr}O5tu!Ipoyib8iXqy}TeJ;4+_7r<1kV0v5?Kv>fYp zg>9L`;XwXa&W7-jf|9~uP2iyF5`5AJ`Q~p4eBU$MCC00`rcSF>`&0fbd^_eqR+}mK z4n*PMMa&FOcc)vTUR zlDUAn-mh`ahi_`f`=39JYTNVjsTa_Y3b1GOIi)6dY)D}xeshB0T8Eov5%UhWd1)u}kjEQ|LDo{tqKKrYIfVz~@dp!! zMOnah@vp)%_-jDTUG09l+;{CkDCH|Q{NqX*uHa1YxFShy*1+;J`gywKaz|2Q{lG8x zP?KBur`}r`!WLKXY_K;C8$EWG>jY3UIh{+BLv0=2)KH%P}6xE2kg)%(-uA6lC?u8}{K(#P*c zE9C8t*u%j2r_{;Rpe1A{9nNXU;b_N0vNgyK!EZVut~}+R2rcbsHilqsOviYh-pYX= zHw@53nlmwYI5W5KP>&`dBZe0Jn?nAdC^HY1wlR6$u^PbpB#AS&5L6zqrXN&7*N2Q` z+Rae1EwS)H=aVSIkr8Ek^1jy2iS2o7mqm~Mr&g5=jjt7VxwglQ^`h#Mx+x2v|9ZAwE$i_9918MjJxTMr?n!bZ6n$}y11u8I9COTU`Z$Fi z!AeAQLMw^gp_{+0QTEJrhL424pVDp%wpku~XRlD3iv{vQ!lAf!_jyqd_h}+Tr1XG| z`*FT*NbPqvHCUsYAkFnM`@l4u_QH&bszpUK#M~XLJt{%?00GXY?u_{gj3Hvs!=N(I z(=AuWPijyoU!r?aFTsa8pLB&cx}$*%;K$e*XqF{~*rA-qn)h^!(-;e}O#B$|S~c+U zN4vyOK0vmtx$5K!?g*+J@G1NmlEI=pyZXZ69tAv=@`t%ag_Hk{LP~OH9iE)I= zaJ69b4kuCkV0V zo(M0#>phpQ_)@j;h%m{-a*LGi(72TP)ws2w*@4|C-3+;=5DmC4s7Lp95%n%@Ko zfdr3-a7m*dys9iIci$A=4NPJ`HfJ;hujLgU)ZRuJI`n;Pw|yksu!#LQnJ#dJysgNb z@@qwR^wrk(jbq4H?d!lNyy72~Dnn87KxsgQ!)|*m(DRM+eC$wh7KnS-mho3|KE)7h zK3k;qZ;K1Lj6uEXLYUYi)1FN}F@-xJ z@@3Hb84sl|j{4$3J}aTY@cbX@pzB_qM~APljrjju6P0tY{C@ zpUCOz_NFmALMv1*blCcwUD3?U6tYs+N%cmJ98D%3)%)Xu^uvzF zS5O!sc#X6?EwsYkvPo6A%O8&y8sCCQH<%f2togVwW&{M;PR!a(ZT_A+jVAbf{@5kL zB@Z(hb$3U{T_}SKA_CoQVU-;j>2J=L#lZ~aQCFg-d<9rzs$_gO&d5N6eFSc z1ml8)P*FSi+k@!^M9nDWR5e@ATD8oxtDu=36Iv2!;dZzidIS(PCtEuXAtlBb1;H%Z zwnC^Ek*D)EX4#Q>R$$WA2sxC_t(!!6Tr?C#@{3}n{<^o;9id1RA&-Pig1e-2B1XpG zliNjgmd3c&%A}s>qf{_j#!Z`fu0xIwm4L0)OF=u(OEmp;bLCIaZX$&J_^Z%4Sq4GZ zPn6sV_#+6pJmDN_lx@1;Zw6Md_p0w9h6mHtzpuIEwNn>OnuRSC2=>fP^Hqgc)xu^4 z<3!s`cORHJh#?!nKI`Et7{3C27+EuH)Gw1f)aoP|B3y?fuVfvpYYmmukx0ya-)TQX zR{ggy5cNf4X|g)nl#jC9p>7|09_S7>1D2GTRBUTW zAkQ=JMRogZqG#v;^=11O6@rPPwvJkr{bW-Qg8`q8GoD#K`&Y+S#%&B>SGRL>;ZunM@49!}Uy zN|bBCJ%sO;@3wl0>0gbl3L@1^O60ONObz8ZI7nder>(udj-jt`;yj^nTQ$L9`OU9W zX4alF#$|GiR47%x@s&LV>2Sz2R6?;2R~5k6V>)nz!o_*1Y!$p>BC5&?hJg_MiE6UBy>RkVZj`9UWbRkN-Hk!S`=BS3t3uyX6)7SF#)71*}`~Ogz z1rap5H6~dhBJ83;q-Y<5V35C2&F^JI-it(=5D#v!fAi9p#UwV~2tZQI+W(Dv?1t9? zfh*xpxxO{-(VGB>!Q&0%^YW_F!@aZS#ucP|YaD#>wd1Fv&Z*SR&mc;asi}1G) z_H>`!akh-Zxq9#io(7%;a$)w+{QH)Y$?UK1Dt^4)up!Szcxnu}kn$0afcfJL#IL+S z5gF_Y30j;{lNrG6m~$Ay?)*V9fZuU@3=kd40=LhazjFrau>(Y>SJNtOz>8x_X-BlA zIpl{i>OarVGj1v(4?^1`R}aQB&WCRQzS~;7R{tDZG=HhgrW@B`W|#cdyj%YBky)P= zpxuOZkW>S6%q7U{VsB#G(^FMsH5QuGXhb(sY+!-R8Bmv6Sx3WzSW<1MPPN1!&PurYky(@`bP9tz z52}LH9Q?+FF5jR6-;|+GVdRA!qtd;}*-h&iIw3Tq3qF9sDIb1FFxGbo&fbG5n8$3F zyY&PWL{ys^dTO}oZ#@sIX^BKW*bon=;te9j5k+T%wJ zNJtoN1~YVj4~YRrlZl)b&kJqp+Z`DqT!la$x&&IxgOQw#yZd-nBP3!7FijBXD|IsU8Zl^ zc6?MKpJQ+7ka|tZQLfchD$PD|;K(9FiLE|eUZX#EZxhG!S-63C$jWX1Yd!6-Yxi-u zjULIr|0-Q%D9jz}IF~S%>0(jOqZ(Ln<$9PxiySr&2Oic7vb<8q=46)Ln%Z|<*z5&> z3f~Zw@m;vR(bESB<=Jqkxn(=#hQw42l(7)h`vMQQTttz9XW6^|^8EK7qhju4r_c*b zJIi`)MB$w@9epwdIfnEBR+?~);yd6C(LeMC& zn&&N*?-g&BBJcV;8&UoZi4Lmxcj16ojlxR~zMrf=O_^i1wGb9X-0@6_rpjPYemIin zmJb+;lHe;Yp=8G)Q(L1bzH*}I>}uAqhj4;g)PlvD9_e_ScR{Ipq|$8NvAvLD8MYr}xl=bU~)f%B3E>r3Bu9_t|ThF3C5~BdOve zEbk^r&r#PT&?^V1cb{72yEWH}TXEE}w>t!cY~rA+hNOTK8FAtIEoszp!qqptS&;r$ zaYV-NX96-h$6aR@1xz6_E0^N49mU)-v#bwtGJm)ibygzJ8!7|WIrcb`$XH~^!a#s& z{Db-0IOTFq#9!^j!n_F}#Z_nX{YzBK8XLPVmc&X`fT7!@$U-@2KM9soGbmOSAmqV z{nr$L^MBo_u^Joyf0E^=eo{Rt0{{e$IFA(#*kP@SQd6lWT2-#>` zP1)7_@IO!9lk>Zt?#CU?cuhiLF&)+XEM9B)cS(gvQT!X3`wL*{fArTS;Ak`J<84du zALKPz4}3nlG8Fo^MH0L|oK2-4xIY!~Oux~1sw!+It)&D3p;+N8AgqKI`ld6v71wy8I!eP0o~=RVcFQR2Gr(eP_JbSytoQ$Yt}l*4r@A8Me94y z8cTDWhqlq^qoAhbOzGBXv^Wa4vUz$(7B!mX`T=x_ueKRRDfg&Uc-e1+z4x$jyW_Pm zp?U;-R#xt^Z8Ev~`m`iL4*c#65Nn)q#=Y0l1AuD&+{|8-Gsij3LUZXpM0Bx0u7WWm zH|%yE@-#XEph2}-$-thl+S;__ciBxSSzHveP%~v}5I%u!z_l_KoW{KRx2=eB33umE zIYFtu^5=wGU`Jab8#}cnYry@9p5UE#U|VVvx_4l49JQ;jQdp(uw=$^A$EA$LM%vmE zvdEOaIcp5qX8wX{mYf0;#51~imYYPn4=k&#DsKTxo{_Mg*;S495?OBY?#gv=edYC* z^O@-sd-qa+U24xvcbL0@C7_6o!$`)sVr-jSJE4XQUQ$?L7}2(}Eixqv;L8AdJAVqc zq}RPgpnDb@E_;?6K58r3h4-!4rT4Ab#rLHLX?eMOfluJk=3i1@Gt1i#iA=O`M0@x! z(HtJP9BMHXEzuD93m|B&woj0g6T?f#^)>J>|I4C5?Gam>n9!8CT%~aT;=oco5d6U8 zMXl(=W;$ND_8+DD*?|5bJ!;8ebESXMUKBAf7YBwNVJibGaJ*(2G`F%wx)grqVPjudiaq^Kl&g$8A2 zWMxMr@_$c}d+;_B`#kUX-t|4VKH&_f^^EP0&=DPLW)H)UzBG%%Tra*5 z%$kyZe3I&S#gfie^z5)!twG={3Cuh)FdeA!Kj<-9** zvT*5%Tb`|QbE!iW-XcOuy39>D3oe6x{>&<#E$o8Ac|j)wq#kQzz|ATd=Z0K!p2$QE zPu?jL8Lb^y3_CQE{*}sTDe!2!dtlFjq&YLY@2#4>XS`}v#PLrpvc4*@q^O{mmnr5D zmyJq~t?8>FWU5vZdE(%4cuZuao0GNjp3~Dt*SLaxI#g_u>hu@k&9Ho*#CZP~lFJHj z(e!SYlLigyc?&5-YxlE{uuk$9b&l6d`uIlpg_z15dPo*iU&|Khx2*A5Fp;8iK_bdP z?T6|^7@lcx2j0T@x>X7|kuuBSB7<^zeY~R~4McconTxA2flHC0_jFxmSTv-~?zVT| zG_|yDqa9lkF*B6_{j=T>=M8r<0s;@z#h)3BQ4NLl@`Xr__o7;~M&dL3J8fP&zLfDfy z);ckcTev{@OUlZ`bCo(-3? z1u1xD`PKgSg?RqeVVsF<1SLF;XYA@Bsa&cY!I48ZJn1V<3d!?s=St?TLo zC0cNr`qD*M#s6f~X>SCNVkva^9A2ZP>CoJ9bvgXe_c}WdX-)pHM5m7O zrHt#g$F0AO+nGA;7dSJ?)|Mo~cf{z2L)Rz!`fpi73Zv)H=a5K)*$5sf_IZypi($P5 zsPwUc4~P-J1@^3C6-r9{V-u0Z&Sl7vNfmuMY4yy*cL>_)BmQF!8Om9Dej%cHxbIzA zhtV0d{=%cr?;bpBPjt@4w=#<>k5ee=TiWAXM2~tUGfm z$s&!Dm0R^V$}fOR*B^kGaipi~rx~A2cS0;t&khV1a4u38*XRUP~f za!rZMtay8bsLt6yFYl@>-y^31(*P!L^^s@mslZy(SMsv9bVoX`O#yBgEcjCmGpyc* zeH$Dw6vB5P*;jor+JOX@;6K#+xc)Z9B8M=x2a@Wx-{snPGpRmOC$zpsqW*JCh@M2Y z#K+M(>=#d^>Of9C`))h<=Bsy)6zaMJ&x-t%&+UcpLjV`jo4R2025 zXaG8EA!0lQa)|dx-@{O)qP6`$rhCkoQqZ`^SW8g-kOwrwsK8 z3ms*AIcyj}-1x&A&vSq{r=QMyp3CHdWH35!sad#!Sm>^|-|afB+Q;|Iq@LFgqIp#Z zD1%H+3I?6RGnk&IFo|u+E0dCxXz4yI^1i!QTu7uvIEH>i3rR{srcST`LIRwdV1P;W z+%AN1NIf@xxvVLiSX`8ILA8MzNqE&7>%jMzGt9wm78bo9<;h*W84i29^w!>V>{N+S zd`5Zmz^G;f=icvoOZfK5#1ctx*~UwD=ab4DGQXehQ!XYnak*dee%YN$_ZPL%KZuz$ zD;$PpT;HM^$KwtQm@7uvT`i6>Hae1CoRVM2)NL<2-k2PiX=eAx+-6j#JI?M}(tuBW zkF%jjLR)O`gI2fcPBxF^HeI|DWwQWHVR!;;{BXXHskxh8F@BMDn`oEi-NHt;CLymW z=KSv5)3dyzec0T5B*`g-MQ<;gz=nIWKUi9ko<|4I(-E0k$QncH>E4l z**1w&#={&zv4Tvhgz#c29`m|;lU-jmaXFMC11 z*dlXDMEOG>VoLMc>!rApwOu2prKSi*!w%`yzGmS+k(zm*CsLK*wv{S_0WX^8A-rKy zbk^Gf_92^7iB_uUF)EE+ET4d|X|>d&mdN?x@vxKAQk`O+r4Qdu>XGy(a(19g;=jU} zFX{O*_NG>!$@jh!U369Lnc+D~qch3uT+_Amyi}*k#LAAwh}k8IPK5a-WZ81ufD>l> z$4cF}GSz>ce`3FAic}6W4Z7m9KGO?(eWqi@L|5Hq0@L|&2flN1PVl}XgQ2q*_n2s3 zt5KtowNkTYB5b;SVuoXA@i5irXO)A&%7?V`1@HGCB&)Wgk+l|^XXChq;u(nyPB}b3 zY>m5jkxpZgi)zfbgv&ec4Zqdvm+D<?Im*mXweS9H+V>)zF#Zp3)bhl$PbISY{5=_z!8&*Jv~NYtI-g!>fDs zmvL5O^U%!^VaKA9gvKw|5?-jk>~%CVGvctKmP$kpnpfN{D8@X*Aazi$txfa%vd-|E z>kYmV66W!lNekJPom29LdZ%(I+ZLZYTXzTg*to~m?7vp%{V<~>H+2}PQ?PPAq`36R z<%wR8v6UkS>Wt#hzGk#44W<%9S=nBfB);6clKwnxY}T*w21Qc3_?IJ@4gYzC7s;WP zVQNI(M=S=JT#xsZy7G`cR(BP9*je0bfeN8JN5~zY(DDs0t{LpHOIbN);?T-69Pf3R zSNe*&p2%AwXHL>__g+xd4Hlc_vu<25H?(`nafS%)3UPP7_4;gk-9ckt8SJRTv5v0M z_Hww`qPudL?ajIR&X*;$y-`<)6dxx1U~5eGS13CB!lX;3w7n&lDDiArbAhSycd}+b zya_3p@A`$kQy;|NJZ~s44Hqo7Hwt}X86NK=(ey>lgWTtGL6k@Gy;PbO!M%1~Wcn2k zUFP|*5d>t-X*RU8g%>|(wwj*~#l4z^Aatf^DWd1Wj#Q*AY0D^V@sC`M zjJc6qXu0I7Y*2;;gGu!plAFzG=J;1%eIOdn zQA>J&e05UN*7I5@yRhK|lbBSfJ+5Uq;!&HV@xfPZrgD}kE*1DSq^=%{o%|LChhl#0 zlMb<^a6ixzpd{kNZr|3jTGeEzuo}-eLT-)Q$#b{!vKx8Tg}swCni>{#%vDY$Ww$84 zew3c9BBovqb}_&BRo#^!G(1Eg((BScRZ}C)Oz?y`T5wOrv);)b^4XR8 zhJo7+<^7)qB>I;46!GySzdneZ>n_E1oWZY;kf94#)s)kWjuJN1c+wbVoNQcmnv}{> zN0pF+Sl3E}UQ$}slSZeLJrwT>Sr}#V(dVaezCQl2|4LN`7L7v&siYR|r7M(*JYfR$ zst3=YaDw$FSc{g}KHO&QiKxuhEzF{f%RJLKe3p*7=oo`WNP)M(9X1zIQPP0XHhY3c znrP{$4#Ol$A0s|4S7Gx2L23dv*Gv2o;h((XVn+9+$qvm}s%zi6nI-_s6?mG! zj{DV;qesJb&owKeEK?=J>UcAlYckA7Sl+I&IN=yasrZOkejir*kE@SN`fk<8Fgx*$ zy&fE6?}G)d_N`){P~U@1jRVA|2*69)KSe_}!~?+`Yb{Y=O~_+@!j<&oVQQMnhoIRU zA0CyF1OFfkK44n*JD~!2!SCPM;PRSk%1XL=0&rz00wxPs&-_eapJy#$h!eqY%nS0{ z!aGg58JIJPF3_ci%n)QSVpa2H`vIe$RD43;#IRfDV&Ibit z+?>HW4{2wOfC6Fw)}4x}i1maDxcE1qi@BS*qcxD2gE@h3#4cgU*D-&3z7D|tVZWt= z-Cy2+*Cm@P4GN_TPUtaVyVesbVDazF@)j8VJ4>XZv!f%}&eO1SvIgr}4`A*3#vat< z_MoByL(qW6L7SFZ#|Gc1fFN)L2PxY+{B8tJp+pxRyz*87)vXR}*=&ahXjBlQKguuf zX6x<<6fQulE^C*KH8~W%ptpaC0l?b=_{~*U4?5Vt;dgM4t_{&UZ1C2j?b>b+5}{IF_CUyvz-@QZPMlJ)r_tS$9kH%RPv#2_nMb zRLj5;chJ72*U`Z@Dqt4$@_+k$%|8m(HqLG!qT4P^DdfvGf&){gKnGCX#H0!;W=AGP zbA&Z`-__a)VTS}kKFjWGk z%|>yE?t*EJ!qeQ%dPk$;xIQ+P0;()PCBDgjJm6Buj{f^awNoVx+9<|lg3%-$G(*f) zll6oOkN|yamn1uyl2*N-lnqRI1cvs_JxLTeahEK=THV$Sz*gQhKNb*p0fNoda#-&F zB-qJgW^g}!TtM|0bS2QZekW7_tKu%GcJ!4?lObt0z_$mZ4rbQ0o=^curCs3bJK6sq z9fu-aW-l#>z~ca(B;4yv;2RZ?tGYAU)^)Kz{L|4oPj zdOf_?de|#yS)p2v8-N||+XL=O*%3+y)oI(HbM)Ds?q8~HPzIP(vs*G`iddbWq}! z(2!VjP&{Z1w+%eUq^ '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/packages/ide/jetbrains/gradlew.bat b/packages/ide/jetbrains/gradlew.bat deleted file mode 100644 index ac1b06f93..000000000 --- a/packages/ide/jetbrains/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/packages/ide/jetbrains/package.json b/packages/ide/jetbrains/package.json deleted file mode 100644 index dcadde4bc..000000000 --- a/packages/ide/jetbrains/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "jetbrains", - "version": "2.22.2", - "displayName": "ZenStack JetBrains IDE Plugin", - "description": "ZenStack JetBrains IDE plugin", - "homepage": "https://zenstack.dev", - "private": true, - "scripts": { - "build": "run-script-os", - "build:win32": "gradlew.bat buildPlugin", - "build:default": "./gradlew buildPlugin" - }, - "author": "ZenStack Team", - "license": "MIT", - "devDependencies": { - "@zenstackhq/language": "workspace:*", - "run-script-os": "^1.1.6", - "zenstack": "workspace:*" - } -} diff --git a/packages/ide/jetbrains/settings.gradle.kts b/packages/ide/jetbrains/settings.gradle.kts deleted file mode 100644 index e0ac2ff8b..000000000 --- a/packages/ide/jetbrains/settings.gradle.kts +++ /dev/null @@ -1,8 +0,0 @@ -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } -} - -rootProject.name = "zenstack" \ No newline at end of file diff --git a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/Utils.kt b/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/Utils.kt deleted file mode 100644 index b35ae1ec0..000000000 --- a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/Utils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.zenstack - -import com.intellij.lang.javascript.service.JSLanguageServiceUtil -import org.jetbrains.plugins.textmate.TextMateService -import org.jetbrains.plugins.textmate.configuration.TextMateUserBundlesSettings - -class Utils { - companion object { - fun addTextMateBundle() { - println("Adding ZenStack textmate bundle") - val textMateBundle = JSLanguageServiceUtil.getPluginDirectory(javaClass, "res/zmodel.tmbundle") - TextMateUserBundlesSettings.instance?.addBundle(textMateBundle.path, "zmodel") - reloadTextMateBundles() - } - - fun disableTextMateBundle() { - println("Disabling ZenStack textmate bundle") - val textMateBundle = JSLanguageServiceUtil.getPluginDirectory(javaClass, "res/zmodel.tmbundle") - TextMateUserBundlesSettings.instance?.disableBundle(textMateBundle.path) - reloadTextMateBundles() - } - - private fun reloadTextMateBundles() { - val textMateService = TextMateService.getInstance(); - textMateService.reloadEnabledBundles() - } - } -} \ No newline at end of file diff --git a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelFileType.kt b/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelFileType.kt deleted file mode 100644 index b0c4ada21..000000000 --- a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelFileType.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.zenstack.lang - -import com.intellij.openapi.fileTypes.LanguageFileType -import javax.swing.Icon - -object ZModelFileType : LanguageFileType(ZModelLanguage) { - override fun getName(): String = "ZModel" - - override fun getDescription(): String = "ZModel Language" - - override fun getDefaultExtension(): String = "zmodel" - - override fun getIcon(): Icon = ZModelIcons.ZModel -} \ No newline at end of file diff --git a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelIcons.kt b/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelIcons.kt deleted file mode 100644 index f168410d6..000000000 --- a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelIcons.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.zenstack.lang - -import com.intellij.openapi.util.IconLoader; - -object ZModelIcons { - @JvmField - val ZModel = IconLoader.getIcon("/icons/zmodel.png", javaClass) -} diff --git a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelLanguage.kt b/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelLanguage.kt deleted file mode 100644 index cfc60749f..000000000 --- a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lang/ZModelLanguage.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.zenstack.lang - -import com.intellij.lang.Language - -object ZModelLanguage : Language("ZModel") { - override fun getDisplayName(): String = "ZModel" -} diff --git a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lsp/ZenStackLspServerDescriptor.kt b/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lsp/ZenStackLspServerDescriptor.kt deleted file mode 100644 index 524d9e812..000000000 --- a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lsp/ZenStackLspServerDescriptor.kt +++ /dev/null @@ -1,55 +0,0 @@ -package dev.zenstack.lsp - -import com.intellij.execution.ExecutionException -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.javascript.nodejs.interpreter.NodeCommandLineConfigurator -import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager -import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreter -import com.intellij.javascript.nodejs.interpreter.wsl.WslNodeInterpreter -import com.intellij.lang.javascript.service.JSLanguageServiceUtil -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.platform.lsp.api.ProjectWideLspServerDescriptor -import com.intellij.platform.lsp.api.customization.LspFormattingSupport -import dev.zenstack.lang.ZModelFileType -import dev.zenstack.Utils - -class ZenStackLspServerDescriptor(project: Project) : ProjectWideLspServerDescriptor(project, "ZenStack") { - - override fun isSupportedFile(file: VirtualFile) = file.fileType == ZModelFileType - - override fun createCommandLine(): GeneralCommandLine { - - Utils.Companion.addTextMateBundle() - - // start language server - val interpreter = NodeJsInterpreterManager.getInstance(project).interpreter - if (interpreter !is NodeJsLocalInterpreter && interpreter !is WslNodeInterpreter) { - throw ExecutionException("Interpreter not configured") - } - - val lsp = JSLanguageServiceUtil.getPluginDirectory(javaClass, "language-server/main.js") - if (lsp == null || !lsp.exists()) { - // broken plugin installation? - throw ExecutionException("Language server not found") - } - - return GeneralCommandLine().apply { - withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) - withCharset(Charsets.UTF_8) - addParameter(lsp.path) - addParameter("--stdio") - - NodeCommandLineConfigurator.find(interpreter) - .configure(this, NodeCommandLineConfigurator.defaultOptions(project)) - } - } - - override val lspGoToDefinitionSupport = true - - override val lspHoverSupport = true - - override val lspFormattingSupport = object : LspFormattingSupport() { - override fun shouldFormatThisFileExclusivelyByServer(file: VirtualFile, ideCanFormatThisFileItself: Boolean, serverExplicitlyWantsToFormatThisFile: Boolean) = file.fileType == ZModelFileType - } -} diff --git a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lsp/ZenStackLspServerSupportProvider.kt b/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lsp/ZenStackLspServerSupportProvider.kt deleted file mode 100644 index 6fb6d79db..000000000 --- a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/lsp/ZenStackLspServerSupportProvider.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.zenstack.lsp - -import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager -import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreter -import com.intellij.javascript.nodejs.interpreter.wsl.WslNodeInterpreter -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.platform.lsp.api.LspServerSupportProvider -import dev.zenstack.lang.ZModelFileType - -class ZenStackLspServerSupportProvider : LspServerSupportProvider { - override fun fileOpened(project: Project, file: VirtualFile, serverStarter: LspServerSupportProvider.LspServerStarter) { - if (file.fileType != ZModelFileType) return - - val node = NodeJsInterpreterManager.getInstance(project).interpreter - if (node !is NodeJsLocalInterpreter && node !is WslNodeInterpreter) return - - serverStarter.ensureServerStarted(ZenStackLspServerDescriptor(project)) - } -} \ No newline at end of file diff --git a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/plugin/PluginStateListener.kt b/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/plugin/PluginStateListener.kt deleted file mode 100644 index 984f3f0ba..000000000 --- a/packages/ide/jetbrains/src/main/kotlin/dev/zenstack/plugin/PluginStateListener.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.zenstack.plugin - -import com.intellij.ide.plugins.DynamicPluginListener -import com.intellij.ide.plugins.IdeaPluginDescriptor -import dev.zenstack.Utils -import org.jetbrains.plugins.textmate.TextMateService - -class PluginStateListener: DynamicPluginListener { - override fun beforePluginLoaded(pluginDescriptor: IdeaPluginDescriptor) { - // install TextMate bundle - Utils.Companion.addTextMateBundle() - } - - override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) { - // uninstall TextMate bundle - Utils.disableTextMateBundle() - } -} \ No newline at end of file diff --git a/packages/ide/jetbrains/src/main/resources/META-INF/plugin.xml b/packages/ide/jetbrains/src/main/resources/META-INF/plugin.xml deleted file mode 100644 index 9c5ddb5f8..000000000 --- a/packages/ide/jetbrains/src/main/resources/META-INF/plugin.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - dev.zenstack.zenstack - - - ZenStack Language Tools - - - ZenStack - - - ZenStack is a toolkit that simplifies the development of a web app's backend. This plugin provides code editing experiences for its ZModel schema language. - -

Features

- -
    -
  • Syntax highlighting
  • -
  • Error highlighting
  • -
  • Go to definition
  • -
  • Code completion
  • -
  • Formatting
  • -
- ]]> - - - com.intellij.modules.ultimate - JavaScript - org.jetbrains.plugins.textmate - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/ide/jetbrains/src/main/resources/META-INF/pluginIcon.svg b/packages/ide/jetbrains/src/main/resources/META-INF/pluginIcon.svg deleted file mode 100644 index c70b27f19..000000000 --- a/packages/ide/jetbrains/src/main/resources/META-INF/pluginIcon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/packages/ide/jetbrains/src/main/resources/META-INF/pluginIcon_dark.svg b/packages/ide/jetbrains/src/main/resources/META-INF/pluginIcon_dark.svg deleted file mode 100644 index 6f52be69d..000000000 --- a/packages/ide/jetbrains/src/main/resources/META-INF/pluginIcon_dark.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/packages/ide/jetbrains/src/main/resources/icons/zmodel.png b/packages/ide/jetbrains/src/main/resources/icons/zmodel.png deleted file mode 100644 index dab4013a337c55a79bee3026861bdf627768b87a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbK}et=Jit9yZufQYD=loU6=04FaW zkPRfI<>Yho^2DX2g~h~0#3gtH1ck)JIeB^I6qSTT#pIQgB&214DsAl@cm;)kQlb)) zf}&y^JiI_IP!~{ESX?3|F3#N2ik+K>jfV@KqgZ)Pz$sP@WtsE3N8%&qzgVWP+RYxj;-||UhLiRW?0_0NJzX3_BrfOnpA>6W z;BieA=>q&45Il-!rI^YyJ=vYX$XcvP+zQ2yj~#_ENcO-FNeq68*oFrE%5 z^Ro!ozGzc$?{uc#imM$)ZstFJtJx^t`h5I8OC9Ts{c%!7Ku0lny85}Sb4q9e0CP2Q AXaE2J diff --git a/packages/ide/jetbrains/src/main/resources/icons/zmodel_dark.png b/packages/ide/jetbrains/src/main/resources/icons/zmodel_dark.png deleted file mode 100644 index 7b4b2b226db72c0e8efadbc824ed7d41d8631ba5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 371 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbK}X@F0Nt9yaZ%coD@zkc=Z)ypRj z?!S5Q{NdfZ&mKMc`tieoeS1H>d-wL`i_h=hy?*f=sOHnVx8Fa1x_{^PizknPjKc>G z{P^-2sNmz6z^?q-)cqr1XyId5tAOttgTv2UtVem=g#aO}gk za-fzrPZ!4!iOaeEH-(xNctoDCF&^ZK+xq(x~Co|qVCyv6)VuxgIGn_17lXmEy*F88x^|BZ&l)&G95Gv~5SoWR-h4d@~U MPgg&ebxsLQ0R6Sf3;+NC diff --git a/packages/ide/jetbrains/src/main/textMate/zmodel.tmbundle/info.plist b/packages/ide/jetbrains/src/main/textMate/zmodel.tmbundle/info.plist deleted file mode 100644 index 9abe98fab..000000000 --- a/packages/ide/jetbrains/src/main/textMate/zmodel.tmbundle/info.plist +++ /dev/null @@ -1,16 +0,0 @@ - - - - - contactEmailRot13 - contact@zenstack.dev - contactName - ZenStack Team - description - ZModel Language - name - zmodel - uuid - 0EA94AB2-A210-4A58-9A25-CFFF59AC430B - - diff --git a/packages/language/.eslintrc.json b/packages/language/.eslintrc.json deleted file mode 100644 index 442a51516..000000000 --- a/packages/language/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "ignorePatterns": "src/generated/*", - "extends": ["../../.eslintrc.json"] -} diff --git a/packages/language/CHANGELOG.md b/packages/language/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/language/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/language/LICENSE b/packages/language/LICENSE deleted file mode 120000 index 30cff7403..000000000 --- a/packages/language/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/language/README.md b/packages/language/README.md deleted file mode 100644 index 27c17fd12..000000000 --- a/packages/language/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ZenStack ZModel language compiler - -This package provides the AST of ZModel - ZenStack's modeling language. - -Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/language/script/generate-plist.ts b/packages/language/script/generate-plist.ts deleted file mode 100644 index f987f71ed..000000000 --- a/packages/language/script/generate-plist.ts +++ /dev/null @@ -1,12 +0,0 @@ -// convert textmate grammar from json to plist with fixes - -import { json2plist } from 'plist2'; -import fs from 'fs'; -import path from 'path'; - -const src = fs.readFileSync(path.resolve(__dirname, '../syntaxes/zmodel.tmLanguage.json'), 'utf-8'); -const json = JSON.parse(src); -json['fileTypes'] = ['zmodel']; - -const plist = json2plist(JSON.stringify(json)); -fs.writeFileSync(path.resolve(__dirname, '../syntaxes/zmodel.tmLanguage'), plist, 'utf-8'); diff --git a/packages/language/syntaxes/zmodel.tmLanguage b/packages/language/syntaxes/zmodel.tmLanguage deleted file mode 100644 index 40b92fb9a..000000000 --- a/packages/language/syntaxes/zmodel.tmLanguage +++ /dev/null @@ -1,113 +0,0 @@ - - - - - name - zmodel - scopeName - source.zmodel - fileTypes - - zmodel - - patterns - - - include - #comments - - - name - keyword.control.zmodel - match - \b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\b - - - name - string.quoted.double.zmodel - begin - " - end - " - patterns - - - include - #string-character-escape - - - - - name - string.quoted.single.zmodel - begin - ' - end - ' - patterns - - - include - #string-character-escape - - - - - repository - - comments - - patterns - - - name - comment.block.zmodel - begin - /\* - beginCaptures - - 0 - - name - punctuation.definition.comment.zmodel - - - end - \*/ - endCaptures - - 0 - - name - punctuation.definition.comment.zmodel - - - - - begin - // - beginCaptures - - 1 - - name - punctuation.whitespace.comment.leading.zmodel - - - end - (?=$) - name - comment.line.zmodel - - - - string-character-escape - - name - constant.character.escape.zmodel - match - \\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\{[0-9A-Fa-f]+\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$) - - - - \ No newline at end of file diff --git a/packages/language/syntaxes/zmodel.tmLanguage.json b/packages/language/syntaxes/zmodel.tmLanguage.json deleted file mode 100644 index 0fb0227e5..000000000 --- a/packages/language/syntaxes/zmodel.tmLanguage.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "zmodel", - "scopeName": "source.zmodel", - "fileTypes": [ - ".zmodel" - ], - "patterns": [ - { - "include": "#comments" - }, - { - "name": "keyword.control.zmodel", - "match": "\\b(Any|BigInt|Boolean|Bytes|ContextType|DateTime|Decimal|FieldReference|Float|Int|Json|Null|Object|String|TransitiveFieldReference|Unsupported|abstract|attribute|datasource|enum|extends|false|function|generator|import|in|model|null|plugin|this|true|type|view)\\b" - }, - { - "name": "string.quoted.double.zmodel", - "begin": "\"", - "end": "\"", - "patterns": [ - { - "include": "#string-character-escape" - } - ] - }, - { - "name": "string.quoted.single.zmodel", - "begin": "'", - "end": "'", - "patterns": [ - { - "include": "#string-character-escape" - } - ] - } - ], - "repository": { - "comments": { - "patterns": [ - { - "name": "comment.block.zmodel", - "begin": "/\\*", - "beginCaptures": { - "0": { - "name": "punctuation.definition.comment.zmodel" - } - }, - "end": "\\*/", - "endCaptures": { - "0": { - "name": "punctuation.definition.comment.zmodel" - } - } - }, - { - "begin": "//", - "beginCaptures": { - "1": { - "name": "punctuation.whitespace.comment.leading.zmodel" - } - }, - "end": "(?=$)", - "name": "comment.line.zmodel" - } - ] - }, - "string-character-escape": { - "name": "constant.character.escape.zmodel", - "match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|u\\{[0-9A-Fa-f]+\\}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" - } - } -} diff --git a/packages/misc/redwood/CHANGELOG.md b/packages/misc/redwood/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/misc/redwood/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/misc/redwood/LICENSE b/packages/misc/redwood/LICENSE deleted file mode 120000 index 5853aaea5..000000000 --- a/packages/misc/redwood/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../../LICENSE \ No newline at end of file diff --git a/packages/misc/redwood/README.md b/packages/misc/redwood/README.md deleted file mode 100644 index 7c4742dc4..000000000 --- a/packages/misc/redwood/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# ZenStack RedwoodJS Integration - -This package provides the CLI and runtime APIs for integrating [ZenStack](https://zenstack.dev) into a [RedwoodJS](https://redwoodjs.com/) project. You can use ZenStack as a drop-in replacement to Prisma and define flexible access control policies declaratively inside the database schema. It's especially useful for building multi-tenant applications which tend to have complex authorization requirements beyond RBAC. - -ZenStack is a full-stack toolkit built above Prisma ORM. It extends Prisma at the schema and the runtime level for adding the following capabilities: - -- Flexible access control -- Data validation rules -- Multi-file schemas -- Custom attributes and functions in schemas - -You can find a more detailed integration guide [here](https://zenstack.dev/docs/guides/redwood). - -### Setting up - -Run the following package setup command: - -```bash -yarn rw setup package @zenstackhq/redwood -``` - -The setup command will: - -1. Update "redwood.toml" to allow ZenStack CLI plugin. -1. Install ZenStack dependencies. -1. Copy your Prisma schema file "api/db/schema.prisma" to "api/db/schema.zmodel". -1. Add a "zenstack" section into "api/package.json" to specify the location 1f both the "schema.prisma" and "schema.zmodel" files. -1. Install a GraphQLYoga plugin in "api/src/functions/graphql.[ts|js]". -1. Eject service templates and modify the templates to use `context.db` (ZenStack-enhanced `PrismaClient`) instead of `db` for data access. - -### Modeling data and access policies - -ZenStack's ZModel language is a superset of Prisma schema language. You should use it to define both the data schema and access policies. [The Complete Guide](https://zenstack.dev/docs/the-complete-guide/part1/) of ZenStack is the best way to learn how to author ZModel schemas. - -You should run the following command after updating "schema.zmodel": - -```bash -yarn rw @zenstackhq generate -``` - -The command does the following things: - -1. Regenerate "schema.prisma" -2. Run `prisma generate` to regenerate PrismaClient -3. Generates supporting JS modules for enforcing access policies at runtime - - - -### Development workflow - -The workflow of using ZenStack is very similar to using Prisma in RedwoodJS projects. The two main differences are: - -1. Generation - - You should run `yarn rw @zenstackhq generate` in place of `yarn rw prisma generate`. The ZenStack's generate command internally regenerates the Prisma schema from the ZModel schema, runs `prisma generate` automatically, and also generates other modules for supporting access policy enforcement at the runtime. - -2. Database access in services - - In your service code, you should use `context.db` instead of `db` for accessing the database. The `context.db` is an enhanced Prisma client that enforces access policies. - - The "setup" command prepared a customized service code template. When you run `yarn rw g service`, the generated code will already use `context.db`. - -Other Prisma-related workflows like generation migration or pushing schema to the database stay unchanged. - -### Deployment - -You should run the "generate" command in your deployment script before `yarn rw deploy`. For example, to deploy to Vercel, the command can be: - -```bash -yarn rw @zenstackhq generate && yarn rw deploy vercel -``` - -### Using the `@zenstackhq` CLI plugin - -The `@zenstackhq/redwood` package registers a set of custom commands to the RedwoodJS CLI under the `@zenstackhq` namespace. You can run it with: - -```bash -yarn rw @zenstackhq [options] -``` - -The plugin is a simple wrapper of the standard `zenstack` CLI, similar to how RedwoodJS wraps the standard `prisma` CLI. It's equivalent to running `npx zenstack ...` inside the "api" directory. - -See the [CLI references](https://zenstack.dev/docs/reference/cli) for the full list of commands. - -### Sample application - -You can find a complete multi-tenant Todo application built with RedwoodJS and ZenStack at: [https://github.com/zenstackhq/sample-todo-redwood](https://github.com/zenstackhq/sample-todo-redwood). - -### Getting help - -The best way to get help and updates about ZenStack is by joining our [Discord server](https://discord.gg/Ykhr738dUe). diff --git a/packages/misc/redwood/bin/cli b/packages/misc/redwood/bin/cli deleted file mode 100755 index ef22870e9..000000000 --- a/packages/misc/redwood/bin/cli +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -require('../dist/setup-package').default(); diff --git a/packages/misc/redwood/package.json b/packages/misc/redwood/package.json deleted file mode 100644 index f65f3bf55..000000000 --- a/packages/misc/redwood/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@zenstackhq/redwood", - "displayName": "ZenStack RedwoodJS Integration", - "version": "2.22.2", - "description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.", - "repository": { - "type": "git", - "url": "https://github.com/zenstackhq/zenstack" - }, - "scripts": { - "clean": "rimraf dist", - "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && pnpm pack dist --pack-destination ../../../.build", - "watch": "tsc --watch", - "lint": "eslint src --ext ts", - "prepublishOnly": "pnpm build" - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "default": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./graphql": { - "default": "./dist/graphql.js", - "types": "./dist/graphql.d.ts" - }, - "./package.json": { - "default": "./package.json" - } - }, - "bin": "bin/cli", - "engines": { - "redwoodjs": ">=6.0.0" - }, - "author": { - "name": "ZenStack Team" - }, - "homepage": "https://zenstack.dev", - "license": "MIT", - "dependencies": { - "@redwoodjs/cli-helpers": "^7.7.3", - "@zenstackhq/runtime": "workspace:*", - "colors": "1.4.0", - "execa": "^5.0.0", - "listr2": "^6.0.0", - "semver": "^7.5.2", - "terminal-link": "^2.0.0", - "ts-morph": "catalog:", - "yargs": "^17.7.2" - }, - "devDependencies": { - "@redwoodjs/graphql-server": "^7.7.3", - "@types/yargs": "^17.0.32", - "graphql-yoga": "^5.0.2" - } -} diff --git a/packages/misc/redwood/src/cli-passthrough.ts b/packages/misc/redwood/src/cli-passthrough.ts deleted file mode 100644 index 367b5f06f..000000000 --- a/packages/misc/redwood/src/cli-passthrough.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { getPaths } from '@redwoodjs/cli-helpers'; -import colors from 'colors'; -import execa from 'execa'; -import { CommandModule } from 'yargs'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -async function runCommand(command: string, options: any) { - const args = ['zenstack', command]; - for (const [name, value] of Object.entries(options)) { - args.push(name.length > 1 ? `--${name}` : `-${name}`); - if (typeof value === 'string') { - // Make sure options that take multiple quoted words - // are passed to zenstack with quotes. - value.split(' ').length > 1 ? args.push(`"${value}"`) : args.push(value); - } - } - - console.log(); - console.log(colors.green('Running ZenStack CLI...')); - console.log(colors.underline('$ npx ' + args.join(' '))); - console.log(); - - try { - await execa('npx', args, { cwd: getPaths().api.base, shell: true, stdio: 'inherit', cleanup: true }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - process.exit(e?.exitCode || 1); - } -} - -/** - * Creates a yargs command that passes all options to the ZenStack CLI command. - */ -export function makePassthroughCommand(command: string): CommandModule { - return { - command, - describe: `Run \`zenstack ${command} ...\``, - builder: (yargs) => { - return yargs - .strictOptions(false) - .strictCommands(false) - .strict(false) - .parserConfiguration({ 'camel-case-expansion': false, 'boolean-negation': false }); - }, - handler: async ({ _, $0: _$0, ...options }) => { - await runCommand(command, options); - }, - }; -} diff --git a/packages/misc/redwood/src/commands/setup.ts b/packages/misc/redwood/src/commands/setup.ts deleted file mode 100644 index ec5979617..000000000 --- a/packages/misc/redwood/src/commands/setup.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { getInstalledRedwoodVersion, getPaths, updateTomlConfig } from '@redwoodjs/cli-helpers'; -import colors from 'colors'; -import execa from 'execa'; -import fs from 'fs'; -import { Listr, ListrTask } from 'listr2'; -import path from 'path'; -import semver from 'semver'; -import terminalLink from 'terminal-link'; -import { Project, SyntaxKind, type PropertyAssignment } from 'ts-morph'; -import type { CommandModule } from 'yargs'; -import { addApiPackages } from '../utils'; - -function updateToml() { - return { - title: 'Updating redwood.toml...', - task: () => { - updateTomlConfig('@zenstackhq/redwood'); - }, - }; -} - -function installDependencies() { - return addApiPackages([ - { pkg: 'zenstack', dev: true }, - { pkg: '@zenstackhq/runtime' }, - { pkg: '@zenstackhq/redwood' }, - ]); -} - -// copy schema.prisma to schema.zmodel, and update package.json -function bootstrapSchema() { - return { - title: 'Bootstrapping ZModel schema...', - task: () => { - const apiPaths = getPaths().api; - const zmodel = path.join(path.dirname(apiPaths.dbSchema), 'schema.zmodel'); - if (!fs.existsSync(zmodel)) { - fs.cpSync(apiPaths.dbSchema, zmodel); - } else { - console.info( - colors.blue(`Schema file "${path.relative(getPaths().base, zmodel)}" already exists. Skipping.`) - ); - } - - const pkgJson = path.join(apiPaths.base, 'package.json'); - if (fs.existsSync(pkgJson)) { - const content = fs.readFileSync(pkgJson, 'utf-8'); - const pkg = JSON.parse(content); - if (!pkg.zenstack) { - pkg.zenstack = { - schema: normalizePath(path.relative(apiPaths.base, zmodel)), - prisma: normalizePath(path.relative(apiPaths.base, apiPaths.dbSchema)), - }; - fs.writeFileSync(pkgJson, JSON.stringify(pkg, null, 4)); - } - } - }, - }; -} - -// ensures posix path separators are used in package.json -function normalizePath(_path: string) { - return _path.replaceAll(path.sep, path.posix.sep); -} - -// install ZenStack GraphQLYoga plugin -function installGraphQLPlugin() { - return { - title: 'Installing GraphQL plugin...', - task: async () => { - // locate "api/functions/graphql.[js|ts]" - let graphQlSourcePath: string | undefined; - const functionsDir = getPaths().api.functions; - if (fs.existsSync(path.join(functionsDir, 'graphql.ts'))) { - graphQlSourcePath = path.join(functionsDir, 'graphql.ts'); - } else if (fs.existsSync(path.join(functionsDir, 'graphql.js'))) { - graphQlSourcePath = path.join(functionsDir, 'graphql.js'); - } - - if (!graphQlSourcePath) { - console.warn( - colors.yellow(`Unable to find handler source file: ${path.join(functionsDir, 'graphql.(js|ts)')}`) - ); - return; - } - - // add import - const project = new Project(); - const graphQlSourceFile = project.addSourceFileAtPathIfExists(graphQlSourcePath)!; - let graphQlSourceFileChanged = false; - let identified = false; - - const imports = graphQlSourceFile.getImportDeclarations(); - if (!imports.some((i) => i.getModuleSpecifierValue() === '@zenstackhq/redwood')) { - graphQlSourceFile.addImportDeclaration({ - moduleSpecifier: '@zenstackhq/redwood', - namedImports: ['useZenStack'], - }); - graphQlSourceFileChanged = true; - } - - // add "extraPlugins" option to `createGraphQLHandler` call - graphQlSourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((expr) => { - if (identified) { - return; - } - - if (expr.getExpression().asKind(SyntaxKind.Identifier)?.getText() === 'createGraphQLHandler') { - const arg = expr.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression); - if (arg) { - identified = true; - const props = arg.getProperties(); - const pluginsProp = props.find( - (p): p is PropertyAssignment => - p.asKind(SyntaxKind.PropertyAssignment)?.getName() === 'extraPlugins' - ); - if (pluginsProp) { - const pluginArr = pluginsProp.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); - if (pluginArr) { - if (!pluginArr.getElements().some((e) => e.getText().includes('useZenStack'))) { - pluginArr.addElement('useZenStack(db)'); - graphQlSourceFileChanged = true; - } - } - } else { - arg.addPropertyAssignment({ - name: 'extraPlugins', - initializer: '[useZenStack(db)]', - }); - graphQlSourceFileChanged = true; - } - } - } - }); - - if (!identified) { - console.warn( - colors.yellow( - 'Unable to determine how to install ZenStack GraphQL plugin. Please add it manually following https://zenstack.dev/docs/guides/redwood.' - ) - ); - } - - if (graphQlSourceFileChanged) { - graphQlSourceFile.formatText(); - } - - // create type-def file to add `db` into global context - let contextTypeDefCreated = false; - if (graphQlSourcePath.endsWith('.ts')) { - const typeDefPath = path.join(getPaths().api.src, 'zenstack.d.ts'); - if (!fs.existsSync(typeDefPath)) { - const rwVersion: string = getInstalledRedwoodVersion(); - const contextModule = - rwVersion && semver.lt(rwVersion, '7.0.0') - ? '@redwoodjs/graphql-server' // pre v7 - : '@redwoodjs/context'; // v7+ - - const typeDefSourceFile = project.createSourceFile( - typeDefPath, - `import type { PrismaClient } from '@zenstackhq/runtime' - -declare module '${contextModule}' { - interface GlobalContext { - db: PrismaClient - } -} -` - ); - typeDefSourceFile.formatText(); - contextTypeDefCreated = true; - } - } - - if (graphQlSourceFileChanged || contextTypeDefCreated) { - await project.save(); - } - }, - }; -} - -// eject templates used for `yarn rw generate service` -function ejectServiceTemplates() { - return { - title: 'Ejecting service templates...', - task: async () => { - if (fs.existsSync(path.join(getPaths().api.base, 'generators', 'service'))) { - console.info(colors.blue('Service templates already ejected. Skipping.')); - return; - } - - await execa('yarn', ['rw', 'setup', 'generator', 'service'], { cwd: getPaths().api.base }); - const serviceTemplateTsFile = path.join( - getPaths().api.base, - 'generators', - 'service', - 'service.ts.template' - ); - const serviceTemplateJsFile = path.join( - getPaths().api.base, - 'generators', - 'service', - 'service.js.template' - ); - const serviceTemplateFile = fs.existsSync(serviceTemplateTsFile) - ? serviceTemplateTsFile - : fs.existsSync(serviceTemplateJsFile) - ? serviceTemplateJsFile - : undefined; - - if (!serviceTemplateFile) { - console.warn(colors.red('Unable to find the ejected service template file.')); - return; - } - - // replace `db.` with `context.db.` - const templateContent = fs.readFileSync(serviceTemplateFile, 'utf-8'); - const newTemplateContent = templateContent - .replace(/^import { db } from.*\n$/gm, '') - .replace(/return db\./g, 'return context.db.'); - fs.writeFileSync(serviceTemplateFile, newTemplateContent); - }, - }; -} - -function whatsNext() { - const zmodel = path.relative(getPaths().base, path.join(path.dirname(getPaths().api.dbSchema), 'schema.zmodel')); - const task: ListrTask = { - title: `What's next...`, - task: (_ctx, task) => { - task.title = - `What's next...\n\n` + - ` - Install ${terminalLink('IDE extensions', 'https://zenstack.dev/docs/guides/ide')}.\n` + - ` - Use "${zmodel}" to model database schema and access control.\n` + - ` - Run \`yarn rw @zenstackhq generate\` to regenerate Prisma schema and client.\n` + - ` - Learn ${terminalLink( - "how ZenStack extends Prisma's power", - 'https://zenstack.dev/docs/the-complete-guide/part1' - )}.\n` + - ` - Create a sample schema with \`yarn rw @zenstackhq sample\`.\n` + - ` - Join ${terminalLink( - 'Discord community', - 'https://discord.gg/Ykhr738dUe' - )} for questions and updates.\n`; - }, - }; - return task; -} - -const setupCommand: CommandModule = { - command: 'setup', - describe: 'Set up ZenStack environment', - builder: (yargs) => yargs, - handler: async () => { - const tasks = new Listr([ - updateToml(), - installDependencies(), - bootstrapSchema(), - installGraphQLPlugin(), - ejectServiceTemplates(), - whatsNext(), - ]); - - try { - await tasks.run(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - console.error(colors.red(e.message)); - process.exit(e?.exitCode || 1); - } - }, -}; - -export default setupCommand; diff --git a/packages/misc/redwood/src/graphql.ts b/packages/misc/redwood/src/graphql.ts deleted file mode 100644 index 943329744..000000000 --- a/packages/misc/redwood/src/graphql.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ForbiddenError, ValidationError } from '@redwoodjs/graphql-server'; -import { - CrudFailureReason, - EnhancementOptions, - PrismaErrorCode, - enhance, - isPrismaClientKnownRequestError, - type AuthUser, -} from '@zenstackhq/runtime'; -import { type Plugin } from 'graphql-yoga'; - -/** - * Plugin options - */ -export type ZenStackPluginOptions = EnhancementOptions; - -/** - * A GraphQLYoga plugin that adds a ZenStack-enhanced PrismaClient into the context - * as `context.db`. - * @param db The original PrismaClient. - * @param getAuthUser A hook function for getting the current user. By default `context.currentUser` is used. - * @param options Options for creating the enhanced PrismaClient. See https://zenstack.dev/docs/reference/runtime-api#enhance for more details. - * @returns - */ -export function useZenStack( - db: PrismaClient, - getAuthUser?: (currentUser: unknown) => Promise, - options?: ZenStackPluginOptions -): Plugin<{ currentUser: unknown; db: PrismaClient }> { - return { - onContextBuilding: () => { - return async ({ context }) => { - const user = getAuthUser ? await getAuthUser(context.currentUser) : (context.currentUser as AuthUser); - context.db = enhance( - db, - { user }, - { - errorTransformer: transformError, - ...options, - } - ); - }; - }, - }; -} - -// Transforms ZenStack errors into appropriate RedwoodJS errors -function transformError(error: unknown) { - if (isPrismaClientKnownRequestError(error) && error.code === PrismaErrorCode.CONSTRAINT_FAILED) { - if ( - error.meta?.reason === CrudFailureReason.ACCESS_POLICY_VIOLATION || - error.meta?.reason === CrudFailureReason.RESULT_NOT_READABLE - ) { - return new ForbiddenError(error.message); - } else if (error.meta?.reason === CrudFailureReason.DATA_VALIDATION_VIOLATION) { - return new ValidationError(error.message); - } - } - return error; -} diff --git a/packages/misc/redwood/src/index.ts b/packages/misc/redwood/src/index.ts deleted file mode 100644 index 67585bb3f..000000000 --- a/packages/misc/redwood/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { makePassthroughCommand } from './cli-passthrough'; -import setup from './commands/setup'; - -export const commands = [ - setup, - makePassthroughCommand('generate'), - makePassthroughCommand('info'), - makePassthroughCommand('format'), - makePassthroughCommand('repl'), -]; -export * from './graphql'; diff --git a/packages/misc/redwood/src/setup-package.ts b/packages/misc/redwood/src/setup-package.ts deleted file mode 100644 index 8d916ac3a..000000000 --- a/packages/misc/redwood/src/setup-package.ts +++ /dev/null @@ -1,11 +0,0 @@ -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; -import setupCommand from './commands/setup'; - -export default async function setupPackage() { - await yargs(hideBin(process.argv)) - .scriptName('zenstack-setup') - // @ts-expect-error yargs types are wrong - .command('$0', 'set up ZenStack', setupCommand.builder, setupCommand.handler) - .parseAsync(); -} diff --git a/packages/misc/redwood/src/utils.ts b/packages/misc/redwood/src/utils.ts deleted file mode 100644 index dbf8a1a04..000000000 --- a/packages/misc/redwood/src/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getPaths } from '@redwoodjs/cli-helpers'; -import execa from 'execa'; - -/** - * Utility for adding npm dependencies to "api" package - */ -export const addApiPackages = (apiPackages: { pkg: string; dev?: boolean }[]) => ({ - title: 'Adding required api packages...', - task: async () => { - const devPkgs = apiPackages.filter((p) => p.dev).map((p) => p.pkg); - if (devPkgs.length > 0) { - await execa('yarn', ['add', '-D', ...devPkgs], { cwd: getPaths().api.base }); - } - - const runtimePkgs = apiPackages.filter((p) => !p.dev).map((p) => p.pkg); - if (runtimePkgs.length > 0) { - await execa('yarn', ['add', ...runtimePkgs], { cwd: getPaths().api.base }); - } - }, -}); diff --git a/packages/misc/redwood/tsconfig.json b/packages/misc/redwood/tsconfig.json deleted file mode 100644 index a11845a55..000000000 --- a/packages/misc/redwood/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "lib": ["ESNext"], - "outDir": "dist" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/plugins/openapi/CHANGELOG.md b/packages/plugins/openapi/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/plugins/openapi/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/plugins/openapi/LICENSE b/packages/plugins/openapi/LICENSE deleted file mode 120000 index 30cff7403..000000000 --- a/packages/plugins/openapi/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/plugins/openapi/README.md b/packages/plugins/openapi/README.md deleted file mode 100644 index 605cb2a1a..000000000 --- a/packages/plugins/openapi/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ZenStack OpenAPI plugin - -This package contains ZenStack plugin for generating OpenAPI specification. - -Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/plugins/openapi/jest.config.ts b/packages/plugins/openapi/jest.config.ts deleted file mode 120000 index 09c104090..000000000 --- a/packages/plugins/openapi/jest.config.ts +++ /dev/null @@ -1 +0,0 @@ -../../../jest.config.ts \ No newline at end of file diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json deleted file mode 100644 index 9672b1c04..000000000 --- a/packages/plugins/openapi/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@zenstackhq/openapi", - "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "2.22.2", - "description": "ZenStack plugin and runtime supporting OpenAPI", - "main": "index.js", - "repository": { - "type": "git", - "url": "https://github.com/zenstackhq/zenstack" - }, - "publishConfig": { - "directory": "dist", - "linkDirectory": true - }, - "scripts": { - "clean": "rimraf dist", - "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE dist && copyfiles -u 1 ./src/plugin.zmodel dist && pnpm pack dist --pack-destination ../../../../.build", - "watch": "tsc --watch", - "lint": "eslint src --ext ts", - "test": "jest", - "prepublishOnly": "pnpm build" - }, - "keywords": [ - "openapi" - ], - "author": "ZenStack Team", - "license": "MIT", - "dependencies": { - "@zenstackhq/runtime": "workspace:*", - "@zenstackhq/sdk": "workspace:*", - "openapi-types": "^12.1.0", - "semver": "^7.5.2", - "ts-pattern": "^4.3.0", - "yaml": "^2.2.2", - "zod-validation-error": "catalog:" - }, - "peerDependencies": { - "zod": "catalog:" - }, - "devDependencies": { - "@readme/openapi-parser": "^2.4.0", - "@types/pluralize": "^0.0.29", - "@types/semver": "^7.3.13", - "@types/tmp": "^0.2.3", - "@zenstackhq/testtools": "workspace:*", - "pluralize": "^8.0.0", - "tmp": "^0.2.1", - "zenstack": "workspace:*" - } -} diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts deleted file mode 100644 index 29f6aee6a..000000000 --- a/packages/plugins/openapi/src/generator-base.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { getZodErrorMessage } from '@zenstackhq/runtime/local-helpers'; -import { PluginError, getDataModels, hasAttribute, type PluginOptions, type PluginResult } from '@zenstackhq/sdk'; -import { Model } from '@zenstackhq/sdk/ast'; -import type { DMMF } from '@zenstackhq/sdk/prisma'; -import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; -import semver from 'semver'; -import { name } from '.'; -import { SecuritySchemesSchema } from './schema'; - -export abstract class OpenAPIGeneratorBase { - protected readonly DEFAULT_SPEC_VERSION = '3.1.0'; - - constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) {} - - abstract generate(): PluginResult; - - protected get includedModels() { - const includeOpenApiIgnored = this.getOption('includeOpenApiIgnored', false); - const models = getDataModels(this.model); - return includeOpenApiIgnored ? models : models.filter((d) => !hasAttribute(d, '@@openapi.ignore')); - } - - protected wrapArray( - schema: OAPI.ReferenceObject | OAPI.SchemaObject, - isArray: boolean - ): OAPI.ReferenceObject | OAPI.SchemaObject { - if (isArray) { - return { type: 'array', items: schema }; - } else { - return schema; - } - } - - protected wrapNullable( - schema: OAPI.ReferenceObject | OAPI.SchemaObject, - isNullable: boolean - ): OAPI.ReferenceObject | OAPI.SchemaObject { - if (!isNullable) { - return schema; - } - - const specVersion = this.getOption('specVersion', this.DEFAULT_SPEC_VERSION); - - // https://stackoverflow.com/questions/48111459/how-to-define-a-property-that-can-be-string-or-null-in-openapi-swagger - // https://stackoverflow.com/questions/40920441/how-to-specify-a-property-can-be-null-or-a-reference-with-swagger - if (semver.gte(specVersion, '3.1.0')) { - // OAPI 3.1.0 and above has native 'null' type - if ((schema as OAPI.BaseSchemaObject).oneOf) { - // merge into existing 'oneOf' - return { oneOf: [...(schema as OAPI.BaseSchemaObject).oneOf!, { type: 'null' }] }; - } else { - // wrap into a 'oneOf' - return { oneOf: [{ type: 'null' }, schema] }; - } - } else { - if ((schema as OAPI.ReferenceObject).$ref) { - // nullable $ref needs to be represented as: { allOf: [{ $ref: ... }], nullable: true } - return { - allOf: [schema], - nullable: true, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - } else { - // nullable scalar: { type: ..., nullable: true } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { ...schema, nullable: true } as any; - } - } - } - - protected array(itemType: OAPI.SchemaObject | OAPI.ReferenceObject) { - return { type: 'array', items: itemType } as const; - } - - protected oneOf(...schemas: (OAPI.SchemaObject | OAPI.ReferenceObject)[]) { - return { oneOf: schemas }; - } - - protected allOf(...schemas: (OAPI.SchemaObject | OAPI.ReferenceObject)[]) { - return { allOf: schemas }; - } - - protected getOption(name: string): T | undefined; - protected getOption(name: string, defaultValue: D): T; - protected getOption(name: string, defaultValue?: T): T | undefined { - const value = this.options[name]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - return value === undefined ? defaultValue : value; - } - - protected generateSecuritySchemes() { - const securitySchemes = this.getOption[]>('securitySchemes'); - if (securitySchemes) { - const parsed = SecuritySchemesSchema.safeParse(securitySchemes); - if (!parsed.success) { - throw new PluginError(name, `"securitySchemes" option is invalid: ${getZodErrorMessage(parsed.error)}`); - } - return parsed.data; - } - return undefined; - } - - protected pruneComponents(paths: OAPI.PathsObject, components: OAPI.ComponentsObject) { - const schemas = components.schemas; - if (schemas) { - const roots = new Set(); - for (const path of Object.values(paths)) { - this.collectUsedComponents(path, roots); - } - - // build a transitive closure for all reachable schemas from roots - const allUsed = new Set(roots); - - let todo = [...allUsed]; - while (todo.length > 0) { - const curr = new Set(allUsed); - Object.entries(schemas) - .filter(([key]) => todo.includes(key)) - .forEach(([, value]) => { - this.collectUsedComponents(value, allUsed); - }); - todo = [...allUsed].filter((e) => !curr.has(e)); - } - - // prune unused schemas - Object.keys(schemas).forEach((key) => { - if (!allUsed.has(key)) { - delete schemas[key]; - } - }); - } - } - - private collectUsedComponents(value: unknown, allUsed: Set) { - if (!value) { - return; - } - - if (Array.isArray(value)) { - value.forEach((item) => { - this.collectUsedComponents(item, allUsed); - }); - } else if (typeof value === 'object') { - Object.entries(value).forEach(([subKey, subValue]) => { - if (subKey === '$ref') { - const ref = subValue as string; - const name = ref.split('/').pop(); - if (name && !allUsed.has(name)) { - allUsed.add(name); - } - } else { - this.collectUsedComponents(subValue, allUsed); - } - }); - } - } -} diff --git a/packages/plugins/openapi/src/index.ts b/packages/plugins/openapi/src/index.ts deleted file mode 100644 index 264403c0a..000000000 --- a/packages/plugins/openapi/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { PluginError, PluginFunction } from '@zenstackhq/sdk'; -import { RESTfulOpenAPIGenerator } from './rest-generator'; -import { RPCOpenAPIGenerator } from './rpc-generator'; - -export const name = 'OpenAPI'; - -const run: PluginFunction = async (model, options, dmmf) => { - if (!dmmf) { - throw new Error('DMMF is required'); - } - - const flavor = options.flavor ? (options.flavor as string) : 'rpc'; - - switch (flavor) { - case 'rest': - return new RESTfulOpenAPIGenerator(model, options, dmmf).generate(); - case 'rpc': - return new RPCOpenAPIGenerator(model, options, dmmf).generate(); - default: - throw new PluginError(name, `Unknown flavor: ${flavor}`); - } -}; - -export default run; diff --git a/packages/plugins/openapi/src/meta.ts b/packages/plugins/openapi/src/meta.ts deleted file mode 100644 index 6a038938a..000000000 --- a/packages/plugins/openapi/src/meta.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getObjectLiteral } from '@zenstackhq/sdk'; -import { DataModel } from '@zenstackhq/sdk/ast'; - -/** - * Metadata for a resource, expressed by @@openapi.meta attribute. - */ -export type ModelMeta = { - tagDescription?: string; - security?: Array>; -}; - -/** - * Metadata for a resource operation, expressed by @@openapi.meta attribute. - */ -export type OperationMeta = { - ignore?: boolean; - method?: string; - path?: string; - summary?: string; - description?: string; - tags?: string[]; - deprecated?: boolean; - security?: Array>; -}; - -/** - * Metadata for a resource, expressed by @@openapi.meta attribute. - */ -export type ResourceMeta = ModelMeta & Record; - -export function getModelResourceMeta(model: DataModel) { - return getObjectLiteral( - model.attributes.find((attr) => attr.decl.ref?.name === '@@openapi.meta')?.args[0].value - ); -} diff --git a/packages/plugins/openapi/src/plugin.zmodel b/packages/plugins/openapi/src/plugin.zmodel deleted file mode 100644 index 506e286c9..000000000 --- a/packages/plugins/openapi/src/plugin.zmodel +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Mark a data model to be ignored when generating OpenAPI specification. - */ -attribute @@openapi.ignore() - -/** - * Provide metadata for a data model for generating OpenAPI specification. - */ -attribute @@openapi.meta(_ meta: Object) diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts deleted file mode 100644 index 30e2607ae..000000000 --- a/packages/plugins/openapi/src/rest-generator.ts +++ /dev/null @@ -1,1069 +0,0 @@ -// Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator - -import { - analyzePolicies, - getDataModels, - hasAttribute, - isForeignKeyField, - isIdField, - isRelationshipField, - PluginError, - PluginOptions, - requireOption, - resolvePath, -} from '@zenstackhq/sdk'; -import { - DataModel, - DataModelField, - DataModelFieldType, - Enum, - isDataModel, - isEnum, - isTypeDef, - Model, - TypeDef, - TypeDefField, - TypeDefFieldType, -} from '@zenstackhq/sdk/ast'; -import type { DMMF } from '@zenstackhq/sdk/prisma'; -import { invariant, lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import fs from 'fs'; -import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; -import path from 'path'; -import pluralize from 'pluralize'; -import { match, P } from 'ts-pattern'; -import YAML from 'yaml'; -import { name } from '.'; -import { OpenAPIGeneratorBase } from './generator-base'; -import { getModelResourceMeta } from './meta'; - -type Policies = ReturnType; - -/** - * Generates RESTful style OpenAPI specification. - */ -export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { - private warnings: string[] = []; - private modelNameMapping: Record; - - constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) { - super(model, options, dmmf); - - if (this.options.omitInputDetails !== undefined) { - throw new PluginError(name, '"omitInputDetails" option is not supported for "rest" flavor'); - } - - this.modelNameMapping = this.getOption('modelNameMapping', {} as Record); - } - - generate() { - let output = requireOption(this.options, 'output', name); - output = resolvePath(output, this.options); - - const components = this.generateComponents(); - const paths = this.generatePaths(); - - // prune unused component schemas - this.pruneComponents(paths, components); - - // generate security schemes, and root-level security - components.securitySchemes = this.generateSecuritySchemes(); - let security: OAPI.Document['security'] | undefined = undefined; - if (components.securitySchemes && Object.keys(components.securitySchemes).length > 0) { - security = Object.keys(components.securitySchemes).map((scheme) => ({ [scheme]: [] })); - } - - const openapi: OAPI.Document = { - openapi: this.getOption('specVersion', this.DEFAULT_SPEC_VERSION), - info: { - title: this.getOption('title', 'ZenStack Generated API'), - version: this.getOption('version', '1.0.0'), - description: this.getOption('description'), - summary: this.getOption('summary'), - }, - tags: this.includedModels.map((model) => { - const meta = getModelResourceMeta(model); - return { - name: lowerCaseFirst(model.name), - description: meta?.tagDescription ?? `${model.name} operations`, - }; - }), - paths, - components, - security, - }; - - // ensure output folder exists - fs.mkdirSync(path.dirname(output), { recursive: true }); - - const ext = path.extname(output); - if (ext && (ext.toLowerCase() === '.yaml' || ext.toLowerCase() === '.yml')) { - fs.writeFileSync(output, YAML.stringify(openapi)); - } else { - fs.writeFileSync(output, JSON.stringify(openapi, undefined, 2)); - } - - return { warnings: this.warnings }; - } - - private generatePaths(): OAPI.PathsObject { - let result: OAPI.PathsObject = {}; - - const includeModelNames = this.includedModels.map((d) => d.name); - - for (const model of this.dmmf.datamodel.models) { - if (includeModelNames.includes(model.name)) { - const zmodel = this.model.declarations.find( - (d) => isDataModel(d) && d.name === model.name - ) as DataModel; - if (zmodel) { - result = { - ...result, - ...this.generatePathsForModel(model, zmodel), - } as OAPI.PathsObject; - } else { - this.warnings.push(`Unable to load ZModel definition for: ${model.name}}`); - } - } - } - return result; - } - - private mapModelName(modelName: string): string { - return this.modelNameMapping[modelName] ?? modelName; - } - - private generatePathsForModel(model: DMMF.Model, zmodel: DataModel): OAPI.PathItemObject | undefined { - const result: Record = {}; - - // analyze access policies to determine default security - const policies = analyzePolicies(zmodel); - - let prefix = this.getOption('prefix', ''); - if (prefix.endsWith('/')) { - prefix = prefix.substring(0, prefix.length - 1); - } - - const resourceMeta = getModelResourceMeta(zmodel); - - const modelName = this.mapModelName(model.name); - - // GET /resource - // POST /resource - result[`${prefix}/${lowerCaseFirst(modelName)}`] = { - get: this.makeResourceList(zmodel, policies, resourceMeta), - post: this.makeResourceCreate(zmodel, policies, resourceMeta), - }; - - // GET /resource/{id} - // PUT /resource/{id} - // PATCH /resource/{id} - // DELETE /resource/{id} - result[`${prefix}/${lowerCaseFirst(modelName)}/{id}`] = { - get: this.makeResourceFetch(zmodel, policies, resourceMeta), - put: this.makeResourceUpdate(zmodel, policies, `update-${modelName}-put`, resourceMeta), - patch: this.makeResourceUpdate(zmodel, policies, `update-${modelName}-patch`, resourceMeta), - delete: this.makeResourceDelete(zmodel, policies, resourceMeta), - }; - - // paths for related resources and relationships - for (const field of zmodel.fields) { - const relationDecl = field.type.reference?.ref; - if (!isDataModel(relationDecl)) { - continue; - } - - // GET /resource/{id}/{relationship} - const relatedDataPath = `${prefix}/${lowerCaseFirst(modelName)}/{id}/${field.name}`; - let container = result[relatedDataPath]; - if (!container) { - container = result[relatedDataPath] = {}; - } - container.get = this.makeRelatedFetch(zmodel, field, relationDecl, resourceMeta); - - const relationshipPath = `${prefix}/${lowerCaseFirst(modelName)}/{id}/relationships/${field.name}`; - container = result[relationshipPath]; - if (!container) { - container = result[relationshipPath] = {}; - } - // GET /resource/{id}/relationships/{relationship} - container.get = this.makeRelationshipFetch(zmodel, field, policies, resourceMeta); - - // PUT /resource/{id}/relationships/{relationship} - container.put = this.makeRelationshipUpdate( - zmodel, - field, - policies, - `update-${model.name}-relationship-${field.name}-put`, - resourceMeta - ); - // PATCH /resource/{id}/relationships/{relationship} - container.patch = this.makeRelationshipUpdate( - zmodel, - field, - policies, - `update-${model.name}-relationship-${field.name}-patch`, - resourceMeta - ); - - if (field.type.array) { - // POST /resource/{id}/relationships/{relationship} - container.post = this.makeRelationshipCreate(zmodel, field, policies, resourceMeta); - } - } - - return result; - } - - private makeResourceList(model: DataModel, policies: Policies, resourceMeta: { security?: object } | undefined) { - return { - operationId: `list-${model.name}`, - description: `List "${model.name}" resources`, - tags: [lowerCaseFirst(model.name)], - parameters: [ - this.parameter('include'), - this.parameter('sort'), - this.parameter('page-offset'), - this.parameter('page-limit'), - ...this.generateFilterParameters(model), - ], - responses: { - '200': this.success(`${model.name}ListResponse`), - '403': this.forbidden(), - }, - security: resourceMeta?.security ?? policies.read === true ? [] : undefined, - }; - } - - private makeResourceCreate(model: DataModel, policies: Policies, resourceMeta: { security?: object } | undefined) { - return { - operationId: `create-${model.name}`, - description: `Create a "${model.name}" resource`, - tags: [lowerCaseFirst(model.name)], - requestBody: { - content: { - 'application/vnd.api+json': { - schema: this.ref(`${model.name}CreateRequest`), - }, - }, - }, - responses: { - '201': this.success(`${model.name}Response`), - '403': this.forbidden(), - '422': this.validationError(), - }, - security: resourceMeta?.security ?? policies.create === true ? [] : undefined, - }; - } - - private makeResourceFetch(model: DataModel, policies: Policies, resourceMeta: { security?: object } | undefined) { - return { - operationId: `fetch-${model.name}`, - description: `Fetch a "${model.name}" resource`, - tags: [lowerCaseFirst(model.name)], - parameters: [this.parameter('id'), this.parameter('include')], - responses: { - '200': this.success(`${model.name}Response`), - '403': this.forbidden(), - '404': this.notFound(), - }, - security: resourceMeta?.security ?? policies.read === true ? [] : undefined, - }; - } - - private makeRelatedFetch( - model: DataModel, - field: DataModelField, - relationDecl: DataModel, - resourceMeta: { security?: object } | undefined - ) { - const policies = analyzePolicies(relationDecl); - const parameters: OAPI.OperationObject['parameters'] = [this.parameter('id'), this.parameter('include')]; - if (field.type.array) { - parameters.push( - this.parameter('sort'), - this.parameter('page-offset'), - this.parameter('page-limit'), - ...this.generateFilterParameters(model) - ); - } - const result = { - operationId: `fetch-${model.name}-related-${field.name}`, - description: `Fetch the related "${field.name}" resource for "${model.name}"`, - tags: [lowerCaseFirst(model.name)], - parameters, - responses: { - '200': this.success( - field.type.array ? `${relationDecl.name}ListResponse` : `${relationDecl.name}Response` - ), - '403': this.forbidden(), - '404': this.notFound(), - }, - security: resourceMeta?.security ?? policies.read === true ? [] : undefined, - }; - return result; - } - - private makeResourceUpdate( - model: DataModel, - policies: Policies, - operationId: string, - resourceMeta: { security?: object } | undefined - ) { - return { - operationId, - description: `Update a "${model.name}" resource`, - tags: [lowerCaseFirst(model.name)], - parameters: [this.parameter('id')], - requestBody: { - content: { - 'application/vnd.api+json': { - schema: this.ref(`${model.name}UpdateRequest`), - }, - }, - }, - responses: { - '200': this.success(`${model.name}Response`), - '403': this.forbidden(), - '404': this.notFound(), - '422': this.validationError(), - }, - security: resourceMeta?.security ?? policies.update === true ? [] : undefined, - }; - } - - private makeResourceDelete(model: DataModel, policies: Policies, resourceMeta: { security?: object } | undefined) { - return { - operationId: `delete-${model.name}`, - description: `Delete a "${model.name}" resource`, - tags: [lowerCaseFirst(model.name)], - parameters: [this.parameter('id')], - responses: { - '200': this.success(), - '403': this.forbidden(), - '404': this.notFound(), - }, - security: resourceMeta?.security ?? policies.delete === true ? [] : undefined, - }; - } - - private makeRelationshipFetch( - model: DataModel, - field: DataModelField, - policies: Policies, - resourceMeta: { security?: object } | undefined - ) { - const parameters: OAPI.OperationObject['parameters'] = [this.parameter('id')]; - if (field.type.array) { - parameters.push( - this.parameter('sort'), - this.parameter('page-offset'), - this.parameter('page-limit'), - ...this.generateFilterParameters(model) - ); - } - return { - operationId: `fetch-${model.name}-relationship-${field.name}`, - description: `Fetch the "${field.name}" relationships for a "${model.name}"`, - tags: [lowerCaseFirst(model.name)], - parameters, - responses: { - '200': field.type.array - ? this.success('_toManyRelationshipResponse') - : this.success('_toOneRelationshipResponse'), - '403': this.forbidden(), - '404': this.notFound(), - }, - security: resourceMeta?.security ?? policies.read === true ? [] : undefined, - }; - } - - private makeRelationshipCreate( - model: DataModel, - field: DataModelField, - policies: Policies, - resourceMeta: { security?: object } | undefined - ) { - return { - operationId: `create-${model.name}-relationship-${field.name}`, - description: `Create new "${field.name}" relationships for a "${model.name}"`, - tags: [lowerCaseFirst(model.name)], - parameters: [this.parameter('id')], - requestBody: { - content: { - 'application/vnd.api+json': { - schema: this.ref('_toManyRelationshipRequest'), - }, - }, - }, - responses: { - '200': this.success('_toManyRelationshipResponse'), - '403': this.forbidden(), - '404': this.notFound(), - }, - security: resourceMeta?.security ?? policies.update === true ? [] : undefined, - }; - } - - private makeRelationshipUpdate( - model: DataModel, - field: DataModelField, - policies: Policies, - operationId: string, - resourceMeta: { security?: object } | undefined - ) { - return { - operationId, - description: `Update "${field.name}" ${pluralize('relationship', field.type.array ? 2 : 1)} for a "${ - model.name - }"`, - tags: [lowerCaseFirst(model.name)], - parameters: [this.parameter('id')], - requestBody: { - content: { - 'application/vnd.api+json': { - schema: field.type.array - ? this.ref('_toManyRelationshipRequest') - : this.ref('_toOneRelationshipRequest'), - }, - }, - }, - responses: { - '200': field.type.array - ? this.success('_toManyRelationshipResponse') - : this.success('_toOneRelationshipResponse'), - '403': this.forbidden(), - '404': this.notFound(), - }, - security: resourceMeta?.security ?? policies.update === true ? [] : undefined, - }; - } - - private generateFilterParameters(model: DataModel) { - const result: OAPI.ParameterObject[] = []; - - const hasMultipleIds = model.fields.filter((f) => isIdField(f)).length > 1; - - for (const field of model.fields) { - if (isForeignKeyField(field)) { - // no filtering with foreign keys because one can filter - // directly on the relationship - continue; - } - - // For multiple ids, make each id field filterable like a regular field - if (isIdField(field) && !hasMultipleIds) { - // id filter - result.push(this.makeFilterParameter(field, 'id', 'Id filter')); - continue; - } - - // equality filter - result.push(this.makeFilterParameter(field, '', 'Equality filter', field.type.array)); - - if (isRelationshipField(field)) { - // TODO: how to express nested filters? - continue; - } - - if (field.type.array) { - // collection filters - result.push(this.makeFilterParameter(field, '$has', 'Collection contains filter')); - result.push(this.makeFilterParameter(field, '$hasEvery', 'Collection contains-all filter', true)); - result.push(this.makeFilterParameter(field, '$hasSome', 'Collection contains-any filter', true)); - result.push( - this.makeFilterParameter(field, '$isEmpty', 'Collection is empty filter', false, { - type: 'boolean', - }) - ); - } else { - if (field.type.type && ['Int', 'BigInt', 'Float', 'Decimal', 'DateTime'].includes(field.type.type)) { - // comparison filters - result.push(this.makeFilterParameter(field, '$lt', 'Less-than filter')); - result.push(this.makeFilterParameter(field, '$lte', 'Less-than or equal filter')); - result.push(this.makeFilterParameter(field, '$gt', 'Greater-than filter')); - result.push(this.makeFilterParameter(field, '$gte', 'Greater-than or equal filter')); - } - - if (field.type.type === 'String') { - result.push(this.makeFilterParameter(field, '$contains', 'String contains filter')); - result.push( - this.makeFilterParameter(field, '$icontains', 'String case-insensitive contains filter') - ); - result.push(this.makeFilterParameter(field, '$search', 'String full-text search filter')); - result.push(this.makeFilterParameter(field, '$startsWith', 'String startsWith filter')); - result.push(this.makeFilterParameter(field, '$endsWith', 'String endsWith filter')); - } - } - } - - return result; - } - - private makeFilterParameter( - field: DataModelField, - name: string, - description: string, - array = false, - schemaOverride?: OAPI.SchemaObject - ) { - let schema: OAPI.SchemaObject | OAPI.ReferenceObject; - - if (schemaOverride) { - schema = schemaOverride; - } else { - const fieldDecl = field.type.reference?.ref; - if (isEnum(fieldDecl)) { - schema = this.ref(fieldDecl.name); - } else if (isDataModel(fieldDecl)) { - schema = { type: 'string' }; - } else if (isTypeDef(fieldDecl) || field.type.type === 'Json') { - schema = { type: 'string', format: 'json' }; - } else { - invariant(field.type.type); - schema = this.fieldTypeToOpenAPISchema(field.type); - } - } - - schema = this.wrapArray(schema, array); - - return { - name: name === 'id' ? 'filter[id]' : `filter[${field.name}${name}]`, - required: false, - description: name === 'id' ? description : `${description} for "${field.name}"`, - in: 'query', - style: 'form', - explode: false, - schema, - } as OAPI.ParameterObject; - } - - private generateComponents() { - const schemas: Record = {}; - const parameters: Record = {}; - const components: OAPI.ComponentsObject = { - schemas, - parameters, - }; - - for (const [name, value] of Object.entries(this.generateSharedComponents())) { - schemas[name] = value; - } - - for (const [name, value] of Object.entries(this.generateParameters())) { - parameters[name] = value; - } - - for (const _enum of this.model.declarations.filter((d): d is Enum => isEnum(d))) { - schemas[_enum.name] = this.generateEnumComponent(_enum); - } - - // data models - for (const model of getDataModels(this.model)) { - for (const [name, value] of Object.entries(this.generateDataModelComponents(model))) { - schemas[name] = value; - } - } - - // type defs - for (const typeDef of this.model.declarations.filter(isTypeDef)) { - schemas[typeDef.name] = this.generateTypeDefComponent(typeDef); - } - - return components; - } - - private generateSharedComponents(): Record { - return { - _jsonapi: { - type: 'object', - description: 'An object describing the server’s implementation', - required: ['version'], - properties: { - version: { type: 'string' }, - }, - }, - _meta: { - type: 'object', - description: 'Meta information about the request or response', - properties: { - serialization: { - description: 'Superjson serialization metadata', - }, - }, - additionalProperties: true, - }, - _resourceIdentifier: { - type: 'object', - description: 'Identifier for a resource', - required: ['type', 'id'], - properties: { - type: { type: 'string', description: 'Resource type' }, - id: { type: 'string', description: 'Resource id' }, - }, - }, - _resource: this.allOf(this.ref('_resourceIdentifier'), { - type: 'object', - description: 'A resource with attributes and relationships', - properties: { - attributes: { type: 'object', description: 'Resource attributes' }, - relationships: { type: 'object', description: 'Resource relationships' }, - }, - }), - _links: { - type: 'object', - required: ['self'], - description: 'Links related to the resource', - properties: { self: { type: 'string', description: 'Link for refetching the curent results' } }, - }, - _pagination: { - type: 'object', - description: 'Pagination information', - required: ['first', 'last', 'prev', 'next'], - properties: { - first: this.wrapNullable({ type: 'string', description: 'Link to the first page' }, true), - last: this.wrapNullable({ type: 'string', description: 'Link to the last page' }, true), - prev: this.wrapNullable({ type: 'string', description: 'Link to the previous page' }, true), - next: this.wrapNullable({ type: 'string', description: 'Link to the next page' }, true), - }, - }, - _errors: { - type: 'array', - description: 'An array of error objects', - items: { - type: 'object', - required: ['status', 'code'], - properties: { - status: { type: 'string', description: 'HTTP status' }, - code: { type: 'string', description: 'Error code' }, - prismaCode: { - type: 'string', - description: 'Prisma error code if the error is thrown by Prisma', - }, - title: { type: 'string', description: 'Error title' }, - detail: { type: 'string', description: 'Error detail' }, - reason: { - type: 'string', - description: 'Detailed error reason', - }, - zodErrors: { - type: 'object', - additionalProperties: true, - description: 'Zod validation errors if the error is due to data validation failure', - }, - }, - }, - }, - _errorResponse: { - type: 'object', - required: ['errors'], - description: 'An error response', - properties: { - jsonapi: this.ref('_jsonapi'), - errors: this.ref('_errors'), - }, - }, - _relationLinks: { - type: 'object', - required: ['self', 'related'], - description: 'Links related to a relationship', - properties: { - self: { type: 'string', description: 'Link for fetching this relationship' }, - related: { - type: 'string', - description: 'Link for fetching the resource represented by this relationship', - }, - }, - }, - _toOneRelationship: { - type: 'object', - description: 'A to-one relationship', - properties: { - data: this.wrapNullable(this.ref('_resourceIdentifier'), true), - }, - }, - _toOneRelationshipWithLinks: { - type: 'object', - required: ['links', 'data'], - description: 'A to-one relationship with links', - properties: { - links: this.ref('_relationLinks'), - data: this.wrapNullable(this.ref('_resourceIdentifier'), true), - }, - }, - _toManyRelationship: { - type: 'object', - required: ['data'], - description: 'A to-many relationship', - properties: { - data: this.array(this.ref('_resourceIdentifier')), - }, - }, - _toManyRelationshipWithLinks: { - type: 'object', - required: ['links', 'data'], - description: 'A to-many relationship with links', - properties: { - links: this.ref('_pagedRelationLinks'), - data: this.array(this.ref('_resourceIdentifier')), - }, - }, - _pagedRelationLinks: { - description: 'Relationship links with pagination information', - ...this.allOf(this.ref('_pagination'), this.ref('_relationLinks')), - }, - _toManyRelationshipRequest: { - type: 'object', - required: ['data'], - description: 'Input for manipulating a to-many relationship', - properties: { - data: { - type: 'array', - items: this.ref('_resourceIdentifier'), - }, - }, - }, - _toOneRelationshipRequest: { - description: 'Input for manipulating a to-one relationship', - ...this.wrapNullable( - { - type: 'object', - required: ['data'], - properties: { - data: this.ref('_resourceIdentifier'), - }, - }, - true - ), - }, - _toManyRelationshipResponse: { - description: 'Response for a to-many relationship', - ...this.allOf(this.ref('_toManyRelationshipWithLinks'), { - type: 'object', - properties: { - jsonapi: this.ref('_jsonapi'), - }, - }), - }, - _toOneRelationshipResponse: { - description: 'Response for a to-one relationship', - ...this.allOf(this.ref('_toOneRelationshipWithLinks'), { - type: 'object', - properties: { - jsonapi: this.ref('_jsonapi'), - }, - }), - }, - }; - } - - private generateParameters(): Record { - return { - id: { - name: 'id', - in: 'path', - description: 'The resource id', - required: true, - schema: { type: 'string' }, - }, - include: { - name: 'include', - in: 'query', - description: 'Relationships to include', - required: false, - style: 'form', - schema: { type: 'string' }, - }, - sort: { - name: 'sort', - in: 'query', - description: 'Fields to sort by', - required: false, - style: 'form', - schema: { type: 'string' }, - }, - 'page-offset': { - name: 'page[offset]', - in: 'query', - description: 'Offset for pagination', - required: false, - style: 'form', - schema: { type: 'integer' }, - }, - 'page-limit': { - name: 'page[limit]', - in: 'query', - description: 'Limit for pagination', - required: false, - style: 'form', - schema: { type: 'integer' }, - }, - }; - } - - private generateEnumComponent(_enum: Enum) { - const schema: OAPI.SchemaObject = { - type: 'string', - description: `The "${_enum.name}" Enum`, - enum: _enum.fields.map((f) => f.name), - }; - return schema; - } - - private generateTypeDefComponent(typeDef: TypeDef) { - const schema: OAPI.SchemaObject = { - type: 'object', - description: `The "${typeDef.name}" TypeDef`, - properties: typeDef.fields.reduce((acc, field) => { - acc[field.name] = this.generateField(field); - return acc; - }, {} as Record), - }; - return schema; - } - - private generateDataModelComponents(model: DataModel) { - const result: Record = {}; - result[`${model.name}`] = this.generateModelEntity(model, 'read'); - - result[`${model.name}CreateRequest`] = { - type: 'object', - description: `Input for creating a "${model.name}"`, - required: ['data'], - properties: { - data: this.generateModelEntity(model, 'create'), - meta: this.ref('_meta'), - }, - }; - - result[`${model.name}UpdateRequest`] = { - type: 'object', - description: `Input for updating a "${model.name}"`, - required: ['data'], - properties: { data: this.generateModelEntity(model, 'update'), meta: this.ref('_meta') }, - }; - - const relationships: Record = {}; - for (const field of model.fields) { - if (isRelationshipField(field)) { - if (field.type.array) { - relationships[field.name] = this.ref('_toManyRelationship'); - } else { - relationships[field.name] = this.ref('_toOneRelationship'); - } - } - } - - result[`${model.name}Response`] = { - type: 'object', - description: `Response for a "${model.name}"`, - required: ['data'], - properties: { - jsonapi: this.ref('_jsonapi'), - data: this.allOf(this.ref(`${model.name}`), { - type: 'object', - properties: { relationships: { type: 'object', properties: relationships } }, - }), - meta: this.ref('_meta'), - included: { - type: 'array', - items: this.ref('_resource'), - }, - links: this.ref('_links'), - }, - }; - - result[`${model.name}ListResponse`] = { - type: 'object', - description: `Response for a list of "${model.name}"`, - required: ['data', 'links'], - properties: { - jsonapi: this.ref('_jsonapi'), - data: this.array( - this.allOf(this.ref(`${model.name}`), { - type: 'object', - properties: { relationships: { type: 'object', properties: relationships } }, - }) - ), - meta: this.ref('_meta'), - included: { - type: 'array', - items: this.ref('_resource'), - }, - links: this.allOf(this.ref('_links'), this.ref('_pagination')), - }, - }; - - return result; - } - - private generateModelEntity(model: DataModel, mode: 'read' | 'create' | 'update'): OAPI.SchemaObject { - const idFields = model.fields.filter((f) => isIdField(f)); - // For compound ids each component is also exposed as a separate fields. - const fields = idFields.length > 1 ? model.fields : model.fields.filter((f) => !isIdField(f)); - - const attributes: Record = {}; - const relationships: Record = {}; - - const required: string[] = []; - - for (const field of fields) { - if (isForeignKeyField(field) && mode !== 'read') { - // foreign keys are not exposed as attributes - continue; - } - if (isRelationshipField(field)) { - let relType: string; - if (mode === 'create' || mode === 'update') { - relType = field.type.array ? '_toManyRelationship' : '_toOneRelationship'; - } else { - relType = field.type.array ? '_toManyRelationshipWithLinks' : '_toOneRelationshipWithLinks'; - } - relationships[field.name] = this.wrapNullable(this.ref(relType), field.type.optional); - } else { - attributes[field.name] = this.generateField(field); - if ( - mode === 'create' && - !field.type.optional && - !hasAttribute(field, '@default') && - // collection relation fields are implicitly optional - !(isDataModel(field.$resolvedType?.decl) && field.type.array) - ) { - required.push(field.name); - } else if (mode === 'read') { - // Until we support sparse fieldsets, all fields are required for read operations - required.push(field.name); - } - } - } - - const toplevelRequired = ['type', 'attributes']; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let properties: any = { - type: { type: 'string' }, - attributes: { - type: 'object', - required: required.length > 0 ? required : undefined, - properties: attributes, - }, - }; - - let idFieldSchema: OAPI.SchemaObject = { type: 'string' }; - if (idFields.length === 1) { - // FIXME: JSON:API actually requires id field to be a string, - // but currently the RESTAPIHandler returns the original data - // type as declared in the ZModel schema. - idFieldSchema = this.fieldTypeToOpenAPISchema(idFields[0].type); - } - - if (mode === 'create') { - // 'id' is required if there's no default value - const idFields = model.fields.filter((f) => isIdField(f)); - if (idFields.length === 1 && !hasAttribute(idFields[0], '@default')) { - properties = { id: idFieldSchema, ...properties }; - toplevelRequired.unshift('id'); - } - } else { - // 'id' always required for read and update - properties = { id: idFieldSchema, ...properties }; - toplevelRequired.unshift('id'); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result: any = { - type: 'object', - description: `The "${model.name}" model`, - required: toplevelRequired, - properties, - } satisfies OAPI.SchemaObject; - - if (Object.keys(relationships).length > 0) { - result.properties.relationships = { - type: 'object', - properties: relationships, - }; - } - - return result; - } - - private generateField(field: DataModelField | TypeDefField) { - return this.wrapArray( - this.wrapNullable(this.fieldTypeToOpenAPISchema(field.type), field.type.optional), - field.type.array - ); - } - - private fieldTypeToOpenAPISchema( - type: DataModelFieldType | TypeDefFieldType - ): OAPI.ReferenceObject | OAPI.SchemaObject { - return match(type.type) - .with('String', () => ({ type: 'string' })) - .with(P.union('Int', 'BigInt'), () => ({ type: 'integer' })) - .with('Float', () => ({ type: 'number' })) - .with('Decimal', () => this.oneOf({ type: 'number' }, { type: 'string' })) - .with('Boolean', () => ({ type: 'boolean' })) - .with('DateTime', () => ({ type: 'string', format: 'date-time' })) - .with('Bytes', () => ({ type: 'string', format: 'byte', description: 'Base64 encoded byte array' })) - .with('Json', () => ({})) - .otherwise((t) => { - const fieldDecl = type.reference?.ref; - invariant(fieldDecl, `Type ${t} is not a model reference`); - return this.ref(fieldDecl?.name); - }); - } - - private ref(type: string) { - return { $ref: `#/components/schemas/${type}` }; - } - - private parameter(type: string) { - return { $ref: `#/components/parameters/${type}` }; - } - - private forbidden() { - return { - description: 'Request is forbidden', - content: { - 'application/vnd.api+json': { - schema: this.ref('_errorResponse'), - }, - }, - }; - } - - private validationError() { - return { - description: 'Request is unprocessable due to validation errors', - content: { - 'application/vnd.api+json': { - schema: this.ref('_errorResponse'), - }, - }, - }; - } - - private notFound() { - return { - description: 'Resource is not found', - content: { - 'application/vnd.api+json': { - schema: this.ref('_errorResponse'), - }, - }, - }; - } - - private success(responseComponent?: string) { - return { - description: 'Successful operation', - content: responseComponent - ? { - 'application/vnd.api+json': { - schema: this.ref(responseComponent), - }, - } - : undefined, - }; - } -} diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts deleted file mode 100644 index 463d3411e..000000000 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ /dev/null @@ -1,963 +0,0 @@ -// Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator - -import { PluginError, PluginOptions, analyzePolicies, requireOption, resolvePath } from '@zenstackhq/sdk'; -import { - DataModel, - Model, - TypeDef, - TypeDefField, - TypeDefFieldType, - isDataModel, - isEnum, - isTypeDef, -} from '@zenstackhq/sdk/ast'; -import { - AggregateOperationSupport, - addMissingInputObjectTypesForAggregate, - addMissingInputObjectTypesForInclude, - addMissingInputObjectTypesForModelArgs, - addMissingInputObjectTypesForSelect, - resolveAggregateOperationSupport, -} from '@zenstackhq/sdk/dmmf-helpers'; -import { supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; -import { lowerCaseFirst, upperCaseFirst, invariant } from '@zenstackhq/runtime/local-helpers'; -import * as fs from 'fs'; -import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; -import * as path from 'path'; -import { P, match } from 'ts-pattern'; -import YAML from 'yaml'; -import { name } from '.'; -import { OpenAPIGeneratorBase } from './generator-base'; -import { getModelResourceMeta } from './meta'; - -const ANY_OBJECT = '_AnyObject'; - -/** - * Generates OpenAPI specification. - */ -export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { - private inputObjectTypes: DMMF.InputType[] = []; - private outputObjectTypes: DMMF.OutputType[] = []; - private usedComponents: Set = new Set(); - private aggregateOperationSupport: AggregateOperationSupport; - private warnings: string[] = []; - private omitInputDetails: boolean; - - constructor(protected model: Model, protected options: PluginOptions, protected dmmf: DMMF.Document) { - super(model, options, dmmf); - - this.omitInputDetails = this.getOption('omitInputDetails', false); - if (this.omitInputDetails !== undefined && typeof this.omitInputDetails !== 'boolean') { - throw new PluginError(name, `Invalid option value for "omitInputDetails", boolean expected`); - } - } - - generate() { - let output = requireOption(this.options, 'output', name); - output = resolvePath(output, this.options); - - // input types - this.inputObjectTypes.push(...(this.dmmf.schema.inputObjectTypes.prisma ?? [])); - this.outputObjectTypes.push(...this.dmmf.schema.outputObjectTypes.prisma); - - // add input object types that are missing from Prisma dmmf - addMissingInputObjectTypesForModelArgs(this.inputObjectTypes, this.dmmf.datamodel.models); - addMissingInputObjectTypesForInclude(this.inputObjectTypes, this.dmmf.datamodel.models); - addMissingInputObjectTypesForSelect(this.inputObjectTypes, this.outputObjectTypes, this.dmmf.datamodel.models); - addMissingInputObjectTypesForAggregate(this.inputObjectTypes, this.outputObjectTypes); - - this.aggregateOperationSupport = resolveAggregateOperationSupport(this.inputObjectTypes); - - const components = this.generateComponents(); - const paths = this.generatePaths(components); - - // generate security schemes, and root-level security - components.securitySchemes = this.generateSecuritySchemes(); - let security: OAPI.Document['security'] | undefined = undefined; - if (components.securitySchemes && Object.keys(components.securitySchemes).length > 0) { - security = Object.keys(components.securitySchemes).map((scheme) => ({ [scheme]: [] })); - } - - // prune unused component schemas - this.pruneComponents(paths, components); - - const openapi: OAPI.Document = { - openapi: this.getOption('specVersion', this.DEFAULT_SPEC_VERSION), - info: { - title: this.getOption('title', 'ZenStack Generated API'), - version: this.getOption('version', '1.0.0'), - description: this.getOption('description'), - summary: this.getOption('summary'), - }, - tags: this.includedModels.map((model) => { - const meta = getModelResourceMeta(model); - return { - name: lowerCaseFirst(model.name), - description: meta?.tagDescription ?? `${model.name} operations`, - }; - }), - components, - paths, - security, - }; - - // ensure output folder exists - fs.mkdirSync(path.dirname(output), { recursive: true }); - - const ext = path.extname(output); - if (ext && (ext.toLowerCase() === '.yaml' || ext.toLowerCase() === '.yml')) { - fs.writeFileSync(output, YAML.stringify(openapi)); - } else { - fs.writeFileSync(output, JSON.stringify(openapi, undefined, 2)); - } - - return { warnings: this.warnings }; - } - - private generatePaths(components: OAPI.ComponentsObject): OAPI.PathsObject { - let result: OAPI.PathsObject = {}; - - for (const model of this.dmmf.datamodel.models) { - const zmodel = this.model.declarations.find((d) => isDataModel(d) && d.name === model.name) as DataModel; - if (zmodel) { - result = { - ...result, - ...this.generatePathsForModel(model, zmodel, components), - } as OAPI.PathsObject; - } else { - this.warnings.push(`Unable to load ZModel definition for: ${model.name}}`); - } - } - return result; - } - - private generatePathsForModel( - model: DMMF.Model, - zmodel: DataModel, - components: OAPI.ComponentsObject - ): OAPI.PathItemObject | undefined { - const result: OAPI.PathItemObject & Record = {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ops: (DMMF.ModelMapping & { createOne?: string | null } & Record) | undefined = - this.dmmf.mappings.modelOperations.find((ops) => ops.model === model.name); - if (!ops) { - this.warnings.push(`Unable to find mapping for model ${model.name}`); - return undefined; - } - - const modelName = upperCaseFirst(model.name); - - type OperationDefinition = { - method: 'get' | 'post' | 'put' | 'patch' | 'delete'; - operation: string; - description: string; - inputType?: object; - outputType: object; - successCode?: number; - security?: Array>; - }; - - const definitions: OperationDefinition[] = []; - const hasRelation = zmodel.fields.some((f) => isDataModel(f.type.reference?.ref)); - - // analyze access policies to determine default security - const { create, read, update, delete: del } = analyzePolicies(zmodel); - - // OrderByWithRelationInput's name is different when "fullTextSearch" is enabled - const orderByWithRelationInput = this.inputObjectTypes - .map((o) => upperCaseFirst(o.name)) - .includes(`${modelName}OrderByWithRelationInput`) - ? `${modelName}OrderByWithRelationInput` - : `${modelName}OrderByWithRelationAndSearchRelevanceInput`; - - if (ops['createOne']) { - definitions.push({ - method: 'post', - operation: 'create', - inputType: this.component( - `${modelName}CreateArgs`, - { - type: 'object', - required: ['data'], - properties: { - select: this.omittableRef(`${modelName}Select`), - include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, - data: this.omittableRef(`${modelName}CreateInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref(modelName)), - description: `Create a new ${modelName}`, - successCode: 201, - security: create === true ? [] : undefined, - }); - } - - if (ops['createMany'] && supportCreateMany(zmodel.$container)) { - definitions.push({ - method: 'post', - operation: 'createMany', - inputType: this.component( - `${modelName}CreateManyArgs`, - { - type: 'object', - required: ['data'], - properties: { - data: this.oneOf( - this.omittableRef(`${modelName}CreateManyInput`), - this.array(this.omittableRef(`${modelName}CreateManyInput`)) - ), - skipDuplicates: { - type: 'boolean', - description: - 'Do not insert records with unique fields or ID fields that already exist.', - }, - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref('BatchPayload')), - description: `Create several ${modelName}`, - successCode: 201, - security: create === true ? [] : undefined, - }); - } - - if (ops['findUnique']) { - definitions.push({ - method: 'get', - operation: 'findUnique', - inputType: this.component( - `${modelName}FindUniqueArgs`, - { - type: 'object', - required: ['where'], - properties: { - select: this.omittableRef(`${modelName}Select`), - include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, - where: this.omittableRef(`${modelName}WhereUniqueInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref(modelName)), - description: `Find one unique ${modelName}`, - security: read === true ? [] : undefined, - }); - } - - if (ops['findFirst']) { - definitions.push({ - method: 'get', - operation: 'findFirst', - inputType: this.component( - `${modelName}FindFirstArgs`, - { - type: 'object', - properties: { - select: this.omittableRef(`${modelName}Select`), - include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, - where: this.omittableRef(`${modelName}WhereInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref(modelName)), - description: `Find the first ${modelName} matching the given condition`, - security: read === true ? [] : undefined, - }); - } - - if (ops['findMany']) { - definitions.push({ - method: 'get', - operation: 'findMany', - inputType: this.component( - `${modelName}FindManyArgs`, - { - type: 'object', - properties: { - select: this.omittableRef(`${modelName}Select`), - include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, - where: this.omittableRef(`${modelName}WhereInput`), - orderBy: this.oneOf( - this.omittableRef(orderByWithRelationInput), - this.array(this.omittableRef(orderByWithRelationInput)) - ), - cursor: this.omittableRef(`${modelName}WhereUniqueInput`), - take: { type: 'integer' }, - skip: { type: 'integer' }, - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.array(this.ref(modelName))), - description: `Find a list of ${modelName}`, - security: read === true ? [] : undefined, - }); - } - - if (ops['updateOne']) { - definitions.push({ - method: 'patch', - operation: 'update', - inputType: this.component( - `${modelName}UpdateArgs`, - { - type: 'object', - required: ['where', 'data'], - properties: { - select: this.omittableRef(`${modelName}Select`), - include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, - where: this.omittableRef(`${modelName}WhereUniqueInput`), - data: this.omittableRef(`${modelName}UpdateInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref(modelName)), - description: `Update a ${modelName}`, - security: update === true ? [] : undefined, - }); - } - - if (ops['updateMany']) { - definitions.push({ - operation: 'updateMany', - method: 'patch', - inputType: this.component( - `${modelName}UpdateManyArgs`, - { - type: 'object', - required: ['data'], - properties: { - where: this.omittableRef(`${modelName}WhereInput`), - data: this.omittableRef(`${modelName}UpdateManyMutationInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref('BatchPayload')), - description: `Update ${modelName}s matching the given condition`, - security: update === true ? [] : undefined, - }); - } - - if (ops['upsertOne']) { - definitions.push({ - method: 'post', - operation: 'upsert', - inputType: this.component( - `${modelName}UpsertArgs`, - { - type: 'object', - required: ['create', 'update', 'where'], - properties: { - select: this.omittableRef(`${modelName}Select`), - include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, - where: this.omittableRef(`${modelName}WhereUniqueInput`), - create: this.omittableRef(`${modelName}CreateInput`), - update: this.omittableRef(`${modelName}UpdateInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref(modelName)), - description: `Upsert a ${modelName}`, - security: create === true && update == true ? [] : undefined, - }); - } - - if (ops['deleteOne']) { - definitions.push({ - method: 'delete', - operation: 'delete', - inputType: this.component( - `${modelName}DeleteUniqueArgs`, - { - type: 'object', - required: ['where'], - properties: { - select: this.omittableRef(`${modelName}Select`), - include: hasRelation ? this.omittableRef(`${modelName}Include`) : undefined, - where: this.omittableRef(`${modelName}WhereUniqueInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref(modelName)), - description: `Delete one unique ${modelName}`, - security: del === true ? [] : undefined, - }); - } - - if (ops['deleteMany']) { - definitions.push({ - method: 'delete', - operation: 'deleteMany', - inputType: this.component( - `${modelName}DeleteManyArgs`, - { - type: 'object', - properties: { - where: this.omittableRef(`${modelName}WhereInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref('BatchPayload')), - description: `Delete ${modelName}s matching the given condition`, - security: del === true ? [] : undefined, - }); - } - - // somehow dmmf doesn't contain "count" operation, so we unconditionally add it here - definitions.push({ - method: 'get', - operation: 'count', - inputType: this.component( - `${modelName}CountArgs`, - { - type: 'object', - properties: { - select: this.omittableRef(`${modelName}Select`), - where: this.omittableRef(`${modelName}WhereInput`), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response( - this.oneOf({ type: 'integer' }, this.ref(`${modelName}CountAggregateOutputType`)) - ), - description: `Find a list of ${modelName}`, - security: read === true ? [] : undefined, - }); - - if (ops['aggregate']) { - definitions.push({ - method: 'get', - operation: 'aggregate', - inputType: this.component( - `${modelName}AggregateArgs`, - { - type: 'object', - properties: { - where: this.omittableRef(`${modelName}WhereInput`), - orderBy: this.omittableRef(orderByWithRelationInput), - cursor: this.omittableRef(`${modelName}WhereUniqueInput`), - take: { type: 'integer' }, - skip: { type: 'integer' }, - ...this.aggregateFields(model), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.ref(`Aggregate${modelName}`)), - description: `Aggregate ${modelName}s`, - security: read === true ? [] : undefined, - }); - } - - if (ops['groupBy']) { - definitions.push({ - method: 'get', - operation: 'groupBy', - inputType: this.component( - `${modelName}GroupByArgs`, - { - type: 'object', - properties: { - where: this.omittableRef(`${modelName}WhereInput`), - orderBy: this.omittableRef(orderByWithRelationInput), - by: this.omittableRef(`${modelName}ScalarFieldEnum`), - having: this.omittableRef(`${modelName}ScalarWhereWithAggregatesInput`), - take: { type: 'integer' }, - skip: { type: 'integer' }, - ...this.aggregateFields(model), - meta: this.ref('_Meta'), - }, - }, - components - ), - outputType: this.response(this.array(this.ref(`${modelName}GroupByOutputType`))), - description: `Group ${modelName}s by fields`, - security: read === true ? [] : undefined, - }); - } - - // get meta specified with @@openapi.meta - const resourceMeta = getModelResourceMeta(zmodel); - - for (const { method, operation, description, inputType, outputType, successCode, security } of definitions) { - const meta = resourceMeta?.[operation]; - - if (meta?.ignore === true) { - continue; - } - - const resolvedMethod = meta?.method ?? method; - let resolvedPath = meta?.path ?? operation; - if (resolvedPath.startsWith('/')) { - resolvedPath = resolvedPath.substring(1); - } - - let prefix = this.getOption('prefix', ''); - if (prefix.endsWith('/')) { - prefix = prefix.substring(0, prefix.length - 1); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const def: OAPI.OperationObject = { - operationId: `${operation}${modelName}`, - description: meta?.description ?? description, - tags: meta?.tags || [lowerCaseFirst(model.name)], - summary: meta?.summary, - // security priority: operation-level > model-level > inferred - security: meta?.security ?? resourceMeta?.security ?? security, - deprecated: meta?.deprecated, - responses: { - [successCode !== undefined ? successCode : '200']: { - description: 'Successful operation', - content: { - 'application/json': { - schema: outputType, - }, - }, - }, - '400': { - content: { - 'application/json': { - schema: this.ref('_Error'), - }, - }, - description: 'Invalid request', - }, - '403': { - content: { - 'application/json': { - schema: this.ref('_Error'), - }, - }, - description: 'Request is forbidden', - }, - '422': { - content: { - 'application/json': { - schema: this.ref('_Error'), - }, - }, - description: 'Request is unprocessable due to validation errors', - }, - }, - }; - - if (inputType) { - if (['post', 'put', 'patch'].includes(resolvedMethod)) { - def.requestBody = { - content: { - 'application/json': { - schema: inputType, - }, - }, - }; - } else { - def.parameters = [ - { - name: 'q', - in: 'query', - required: true, - description: 'Superjson-serialized Prisma query object', - content: { - 'application/json': { - schema: inputType, - }, - }, - }, - { - name: 'meta', - in: 'query', - description: 'Superjson serialization metadata for parameter "q"', - content: { - 'application/json': { - schema: {}, - }, - }, - }, - ] satisfies OAPI.ParameterObject[]; - } - } - - const includeModelNames = this.includedModels.map((d) => d.name); - if (includeModelNames.includes(model.name)) { - result[`${prefix}/${lowerCaseFirst(model.name)}/${resolvedPath}`] = { - [resolvedMethod]: def, - }; - } - } - return result; - } - - private aggregateFields(model: DMMF.Model) { - const result: Record = {}; - const supportedOps = this.aggregateOperationSupport[model.name]; - const modelName = upperCaseFirst(model.name); - if (supportedOps) { - if (supportedOps.count) { - result._count = this.oneOf({ type: 'boolean' }, this.omittableRef(`${modelName}CountAggregateInput`)); - } - if (supportedOps.min) { - result._min = this.omittableRef(`${modelName}MinAggregateInput`); - } - if (supportedOps.max) { - result._max = this.omittableRef(`${modelName}MaxAggregateInput`); - } - if (supportedOps.sum) { - result._sum = this.omittableRef(`${modelName}SumAggregateInput`); - } - if (supportedOps.avg) { - result._avg = this.omittableRef(`${modelName}AvgAggregateInput`); - } - } - return result; - } - - private component(name: string, def: object, components: OAPI.ComponentsObject): object { - invariant(components.schemas); - components.schemas[name] = def; - return this.ref(name); - } - - private generateComponents() { - const schemas: Record = {}; - const components: OAPI.ComponentsObject = { - schemas, - }; - - if (this.omitInputDetails) { - // generate a catch-all object type - schemas[ANY_OBJECT] = { - type: 'object', - additionalProperties: true, - }; - } - - // user-defined and built-in enums - for (const _enum of [...(this.dmmf.schema.enumTypes.model ?? []), ...this.dmmf.schema.enumTypes.prisma]) { - schemas[upperCaseFirst(_enum.name)] = this.generateEnumComponent(_enum); - } - - // Also add enums from AST that might not be in DMMF (e.g., only used in TypeDefs) - for (const enumDecl of this.model.declarations.filter(isEnum)) { - if (!schemas[upperCaseFirst(enumDecl.name)]) { - schemas[upperCaseFirst(enumDecl.name)] = { - type: 'string', - enum: enumDecl.fields.map((f) => f.name), - }; - } - } - - // data models - for (const model of this.dmmf.datamodel.models) { - schemas[upperCaseFirst(model.name)] = this.generateEntityComponent(model); - } - - // type defs - for (const typeDef of this.model.declarations.filter(isTypeDef)) { - schemas[upperCaseFirst(typeDef.name)] = this.generateTypeDefComponent(typeDef); - } - - for (const input of this.inputObjectTypes) { - schemas[upperCaseFirst(input.name)] = this.generateInputComponent(input); - } - - for (const output of this.outputObjectTypes.filter((t) => !['Query', 'Mutation'].includes(t.name))) { - schemas[upperCaseFirst(output.name)] = this.generateOutputComponent(output); - } - - schemas['_Meta'] = { - type: 'object', - description: 'Meta information about the request or response', - properties: { - serialization: { - description: 'Serialization metadata', - }, - }, - additionalProperties: true, - }; - - schemas['_Error'] = { - type: 'object', - required: ['error'], - properties: { - error: { - type: 'object', - required: ['message'], - properties: { - prisma: { - type: 'boolean', - description: 'Indicates if the error occurred during a Prisma call', - }, - rejectedByPolicy: { - type: 'boolean', - description: 'Indicates if the error was due to rejection by a policy', - }, - code: { - type: 'string', - description: 'Prisma error code. Only available when "prisma" field is true.', - }, - message: { - type: 'string', - description: 'Error message', - }, - reason: { - type: 'string', - description: 'Detailed error reason', - }, - zodErrors: { - type: 'object', - additionalProperties: true, - description: 'Zod validation errors if the error is due to data validation failure', - }, - }, - additionalProperties: true, - }, - }, - }; - - // misc types - schemas['BatchPayload'] = { - type: 'object', - properties: { - count: { type: 'integer' }, - }, - }; - - return components; - } - - private generateEnumComponent(_enum: DMMF.SchemaEnum): OAPI.SchemaObject { - const schema: OAPI.SchemaObject = { - type: 'string', - enum: _enum.values as string[], - }; - return schema; - } - - private generateEntityComponent(model: DMMF.Model): OAPI.SchemaObject { - const properties: Record = {}; - - const required: string[] = []; - for (const field of model.fields) { - properties[field.name] = this.generateField(field, model.name); - if (field.isRequired && !(field.relationName && field.isList)) { - required.push(field.name); - } - } - - const result: OAPI.SchemaObject = { type: 'object', properties }; - if (required.length > 0) { - result.required = required; - } - return result; - } - - private generateField( - def: { kind: DMMF.FieldKind; type: string; isList: boolean; isRequired: boolean; name?: string }, - modelName?: string - ) { - // For Json fields, check if there's a corresponding TypeDef in the original model - if (def.kind === 'scalar' && def.type === 'Json' && modelName && def.name) { - const dataModel = this.model.declarations.find((d) => isDataModel(d) && d.name === modelName) as DataModel; - if (dataModel) { - const field = dataModel.fields.find((f) => f.name === def.name); - if (field?.type.reference?.ref && isTypeDef(field.type.reference.ref)) { - // This Json field references a TypeDef - // Use field.type.array from ZModel AST instead of def.isList from DMMF, - // since Prisma treats TypeDef fields as plain Json and doesn't know about arrays - return this.wrapArray( - this.wrapNullable(this.ref(field.type.reference.ref.name, true), !def.isRequired), - field.type.array - ); - } - } - } - - switch (def.kind) { - case 'scalar': - return this.wrapArray(this.prismaTypeToOpenAPIType(def.type, !def.isRequired), def.isList); - - case 'enum': - case 'object': - return this.wrapArray(this.wrapNullable(this.ref(def.type, false), !def.isRequired), def.isList); - - default: - throw new PluginError(name, `Unsupported field kind: ${def.kind}`); - } - } - - private generateInputComponent(input: DMMF.InputType): OAPI.SchemaObject { - const properties: Record = {}; - for (const field of input.fields) { - const options = field.inputTypes - .filter( - (f) => - f.type !== 'Null' && - // fieldRefTypes refer to other fields in the model and don't need to be generated as part of schema - f.location !== 'fieldRefTypes' - ) - .map((f) => { - return this.wrapArray(this.prismaTypeToOpenAPIType(f.type, false), f.isList); - }); - - let prop = options.length > 1 ? { oneOf: options } : options[0]; - - // if types include 'Null', make it nullable - prop = this.wrapNullable( - prop, - field.inputTypes.some((f) => f.type === 'Null') - ); - - properties[field.name] = prop; - } - - const result: OAPI.SchemaObject = { type: 'object', properties }; - this.setInputRequired(input.fields, result); - return result; - } - - private generateOutputComponent(output: DMMF.OutputType): OAPI.SchemaObject { - const properties: Record = {}; - for (const field of output.fields) { - let outputType: OAPI.ReferenceObject | OAPI.SchemaObject; - switch (field.outputType.location) { - case 'scalar': - case 'enumTypes': - outputType = this.prismaTypeToOpenAPIType(field.outputType.type, !!field.isNullable); - break; - case 'outputObjectTypes': - outputType = this.prismaTypeToOpenAPIType(field.outputType.type, !!field.isNullable); - break; - } - field.outputType; - properties[field.name] = this.wrapArray(outputType, field.outputType.isList); - } - - const result: OAPI.SchemaObject = { type: 'object', properties }; - this.setOutputRequired(output.fields, result); - return result; - } - - private generateTypeDefComponent(typeDef: TypeDef): OAPI.SchemaObject { - const schema: OAPI.SchemaObject = { - type: 'object', - description: `The "${typeDef.name}" TypeDef`, - properties: typeDef.fields.reduce((acc, field) => { - acc[field.name] = this.generateTypeDefField(field); - return acc; - }, {} as Record), - }; - - const required = typeDef.fields.filter((f) => !f.type.optional).map((f) => f.name); - if (required.length > 0) { - schema.required = required; - } - - return schema; - } - - private generateTypeDefField(field: TypeDefField): OAPI.ReferenceObject | OAPI.SchemaObject { - return this.wrapArray( - this.wrapNullable(this.typeDefFieldTypeToOpenAPISchema(field.type), field.type.optional), - field.type.array - ); - } - - private typeDefFieldTypeToOpenAPISchema(type: TypeDefFieldType): OAPI.ReferenceObject | OAPI.SchemaObject { - // For references to other types (TypeDef, Enum, Model) - if (type.reference?.ref) { - return this.ref(type.reference.ref.name, true); - } - - // For scalar types, reuse the existing mapping logic - // Note: Json type is handled as empty schema for consistency - return match(type.type) - .with('Json', () => ({} as OAPI.SchemaObject)) - .otherwise((t) => { - // Delegate to prismaTypeToOpenAPIType for all other scalar types - return this.prismaTypeToOpenAPIType(String(t), false); - }); - } - - private setInputRequired(fields: readonly DMMF.SchemaArg[], result: OAPI.NonArraySchemaObject) { - const required = fields.filter((f) => f.isRequired).map((f) => f.name); - if (required.length > 0) { - result.required = required; - } - } - - private setOutputRequired(fields: readonly DMMF.SchemaField[], result: OAPI.NonArraySchemaObject) { - const required = fields.filter((f) => f.isNullable !== true).map((f) => f.name); - if (required.length > 0) { - result.required = required; - } - } - - private prismaTypeToOpenAPIType(type: string, nullable: boolean): OAPI.ReferenceObject | OAPI.SchemaObject { - const result = match(type) - .with('String', () => ({ type: 'string' })) - .with(P.union('Int', 'BigInt'), () => ({ type: 'integer' })) - .with('Float', () => ({ type: 'number' })) - .with('Decimal', () => this.oneOf({ type: 'string' }, { type: 'number' })) - .with(P.union('Boolean', 'True'), () => ({ type: 'boolean' })) - .with('DateTime', () => ({ type: 'string', format: 'date-time' })) - .with('Bytes', () => ({ type: 'string', format: 'byte' })) - .with(P.union('JSON', 'Json'), () => { - // For Json fields, check if there's a specific TypeDef reference - // Otherwise, return empty schema for arbitrary JSON - const isTypeDefType = this.model.declarations.some((d) => isTypeDef(d) && d.name === type); - return isTypeDefType ? this.ref(type, false) : {}; - }) - .otherwise((type) => this.ref(type.toString(), false)); - - return this.wrapNullable(result, nullable); - } - - private ref(type: string, rooted = true, description?: string): OAPI.ReferenceObject { - if (rooted) { - this.usedComponents.add(type); - } - return { $ref: `#/components/schemas/${upperCaseFirst(type)}`, description }; - } - - private omittableRef(type: string, rooted = true, description?: string): OAPI.ReferenceObject { - if (this.omitInputDetails) { - return this.ref(ANY_OBJECT); - } else { - return this.ref(type, rooted, description); - } - } - - private response(schema: OAPI.SchemaObject): OAPI.SchemaObject { - return { - type: 'object', - required: ['data'], - properties: { - data: { ...schema, description: 'The Prisma response data serialized with superjson' }, - meta: this.ref('_Meta', true, 'The superjson serialization metadata for the "data" field'), - }, - }; - } -} diff --git a/packages/plugins/openapi/src/schema.ts b/packages/plugins/openapi/src/schema.ts deleted file mode 100644 index 777e9ef6d..000000000 --- a/packages/plugins/openapi/src/schema.ts +++ /dev/null @@ -1,44 +0,0 @@ -import z from 'zod'; - -/** - * Zod schema for OpenAPI security schemes: https://swagger.io/docs/specification/authentication/ - */ -export const SecuritySchemesSchema = z.record( - z.string(), - z.union([ - z.object({ type: z.literal('http'), scheme: z.literal('basic') }), - z.object({ type: z.literal('http'), scheme: z.literal('bearer'), bearerFormat: z.string().optional() }), - z.object({ - type: z.literal('apiKey'), - in: z.union([z.literal('header'), z.literal('query'), z.literal('cookie')]), - name: z.string(), - }), - z.object({ - type: z.literal('oauth2'), - description: z.string(), - flows: z.object({ - authorizationCode: z.object({ - authorizationUrl: z.string(), - tokenUrl: z.string(), - refreshUrl: z.string(), - scopes: z.record(z.string(), z.string()), - }), - implicit: z.object({ - authorizationUrl: z.string(), - refreshUrl: z.string(), - scopes: z.record(z.string(), z.string()), - }), - password: z.object({ - tokenUrl: z.string(), - refreshUrl: z.string(), - scopes: z.record(z.string(), z.string()), - }), - clientCredentials: z.object({ - tokenUrl: z.string(), - refreshUrl: z.string(), - scopes: z.record(z.string(), z.string()), - }), - }), - }), - ]) -); diff --git a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml deleted file mode 100644 index b6e0ad750..000000000 --- a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml +++ /dev/null @@ -1,3267 +0,0 @@ -openapi: 3.0.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: user - description: User operations - - name: profile - description: Profile operations - - name: post_Item - description: Post-related operations - - name: postLike - description: PostLike operations -paths: - /user: - get: - operationId: list-User - description: List "User" resources - tags: - - user - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[createdAt] - required: false - description: Equality filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lt] - required: false - description: Less-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lte] - required: false - description: Less-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gt] - required: false - description: Greater-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gte] - required: false - description: Greater-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt] - required: false - description: Equality filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lt] - required: false - description: Less-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lte] - required: false - description: Less-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gt] - required: false - description: Greater-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gte] - required: false - description: Greater-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[email] - required: false - description: Equality filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$contains] - required: false - description: String contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$icontains] - required: false - description: String case-insensitive contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$search] - required: false - description: String full-text search filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$startsWith] - required: false - description: String startsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$endsWith] - required: false - description: String endsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[role] - required: false - description: Equality filter for "role" - in: query - style: form - explode: false - schema: - $ref: '#/components/schemas/role' - - name: filter[posts] - required: false - description: Equality filter for "posts" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - - name: filter[profile] - required: false - description: Equality filter for "profile" - in: query - style: form - explode: false - schema: - type: string - - name: filter[likes] - required: false - description: Equality filter for "likes" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-User - description: Create a "User" resource - tags: - - user - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}: - get: - operationId: fetch-User - description: Fetch a "User" resource - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-User-put - description: Update a "User" resource - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-User-patch - description: Update a "User" resource - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - delete: - operationId: delete-User - description: Delete a "User" resource - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}/posts: - get: - operationId: fetch-User-related-posts - description: Fetch the related "posts" resource for "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[createdAt] - required: false - description: Equality filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lt] - required: false - description: Less-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lte] - required: false - description: Less-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gt] - required: false - description: Greater-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gte] - required: false - description: Greater-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt] - required: false - description: Equality filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lt] - required: false - description: Less-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lte] - required: false - description: Less-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gt] - required: false - description: Greater-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gte] - required: false - description: Greater-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[email] - required: false - description: Equality filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$contains] - required: false - description: String contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$icontains] - required: false - description: String case-insensitive contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$search] - required: false - description: String full-text search filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$startsWith] - required: false - description: String startsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$endsWith] - required: false - description: String endsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[role] - required: false - description: Equality filter for "role" - in: query - style: form - explode: false - schema: - $ref: '#/components/schemas/role' - - name: filter[posts] - required: false - description: Equality filter for "posts" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - - name: filter[profile] - required: false - description: Equality filter for "profile" - in: query - style: form - explode: false - schema: - type: string - - name: filter[likes] - required: false - description: Equality filter for "likes" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}/relationships/posts: - get: - operationId: fetch-User-relationship-posts - description: Fetch the "posts" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[createdAt] - required: false - description: Equality filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lt] - required: false - description: Less-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lte] - required: false - description: Less-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gt] - required: false - description: Greater-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gte] - required: false - description: Greater-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt] - required: false - description: Equality filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lt] - required: false - description: Less-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lte] - required: false - description: Less-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gt] - required: false - description: Greater-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gte] - required: false - description: Greater-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[email] - required: false - description: Equality filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$contains] - required: false - description: String contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$icontains] - required: false - description: String case-insensitive contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$search] - required: false - description: String full-text search filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$startsWith] - required: false - description: String startsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$endsWith] - required: false - description: String endsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[role] - required: false - description: Equality filter for "role" - in: query - style: form - explode: false - schema: - $ref: '#/components/schemas/role' - - name: filter[posts] - required: false - description: Equality filter for "posts" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - - name: filter[profile] - required: false - description: Equality filter for "profile" - in: query - style: form - explode: false - schema: - type: string - - name: filter[likes] - required: false - description: Equality filter for "likes" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-User-relationship-posts-put - description: Update "posts" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-User-relationship-posts-patch - description: Update "posts" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-User-relationship-posts - description: Create new "posts" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}/profile: - get: - operationId: fetch-User-related-profile - description: Fetch the related "profile" resource for "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}/relationships/profile: - get: - operationId: fetch-User-relationship-profile - description: Fetch the "profile" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-User-relationship-profile-put - description: Update "profile" relationship for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-User-relationship-profile-patch - description: Update "profile" relationship for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /profile: - get: - operationId: list-Profile - description: List "Profile" resources - tags: - - profile - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[image] - required: false - description: Equality filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$contains] - required: false - description: String contains filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$icontains] - required: false - description: String case-insensitive contains filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$search] - required: false - description: String full-text search filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$startsWith] - required: false - description: String startsWith filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$endsWith] - required: false - description: String endsWith filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[user] - required: false - description: Equality filter for "user" - in: query - style: form - explode: false - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-Profile - description: Create a "Profile" resource - tags: - - profile - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /profile/{id}: - get: - operationId: fetch-Profile - description: Fetch a "Profile" resource - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-Profile-put - description: Update a "Profile" resource - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-Profile-patch - description: Update a "Profile" resource - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - delete: - operationId: delete-Profile - description: Delete a "Profile" resource - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /profile/{id}/user: - get: - operationId: fetch-Profile-related-user - description: Fetch the related "user" resource for "Profile" - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /profile/{id}/relationships/user: - get: - operationId: fetch-Profile-relationship-user - description: Fetch the "user" relationships for a "Profile" - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-Profile-relationship-user-put - description: Update "user" relationship for a "Profile" - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-Profile-relationship-user-patch - description: Update "user" relationship for a "Profile" - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /post_Item: - get: - operationId: list-post_Item - description: List "post_Item" resources - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[createdAt] - required: false - description: Equality filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lt] - required: false - description: Less-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lte] - required: false - description: Less-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gt] - required: false - description: Greater-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gte] - required: false - description: Greater-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt] - required: false - description: Equality filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lt] - required: false - description: Less-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lte] - required: false - description: Less-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gt] - required: false - description: Greater-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gte] - required: false - description: Greater-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[title] - required: false - description: Equality filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$contains] - required: false - description: String contains filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$icontains] - required: false - description: String case-insensitive contains filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$search] - required: false - description: String full-text search filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$startsWith] - required: false - description: String startsWith filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$endsWith] - required: false - description: String endsWith filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[author] - required: false - description: Equality filter for "author" - in: query - style: form - explode: false - schema: - type: string - - name: filter[published] - required: false - description: Equality filter for "published" - in: query - style: form - explode: false - schema: - type: boolean - - name: filter[viewCount] - required: false - description: Equality filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[viewCount$lt] - required: false - description: Less-than filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[viewCount$lte] - required: false - description: Less-than or equal filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[viewCount$gt] - required: false - description: Greater-than filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[viewCount$gte] - required: false - description: Greater-than or equal filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[notes] - required: false - description: Equality filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$contains] - required: false - description: String contains filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$icontains] - required: false - description: String case-insensitive contains filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$search] - required: false - description: String full-text search filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$startsWith] - required: false - description: String startsWith filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$endsWith] - required: false - description: String endsWith filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[likes] - required: false - description: Equality filter for "likes" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-post_Item - description: Create a "post_Item" resource - tags: - - post_Item - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /post_Item/{id}: - get: - operationId: fetch-post_Item - description: Fetch a "post_Item" resource - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-post_Item-put - description: Update a "post_Item" resource - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-post_Item-patch - description: Update a "post_Item" resource - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - delete: - operationId: delete-post_Item - description: Delete a "post_Item" resource - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /post_Item/{id}/author: - get: - operationId: fetch-post_Item-related-author - description: Fetch the related "author" resource for "post_Item" - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /post_Item/{id}/relationships/author: - get: - operationId: fetch-post_Item-relationship-author - description: Fetch the "author" relationships for a "post_Item" - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-post_Item-relationship-author-put - description: Update "author" relationship for a "post_Item" - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-post_Item-relationship-author-patch - description: Update "author" relationship for a "post_Item" - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike: - get: - operationId: list-PostLike - description: List "PostLike" resources - tags: - - postLike - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[post] - required: false - description: Equality filter for "post" - in: query - style: form - explode: false - schema: - type: string - - name: filter[user] - required: false - description: Equality filter for "user" - in: query - style: form - explode: false - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-PostLike - description: Create a "PostLike" resource - tags: - - postLike - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}: - get: - operationId: fetch-PostLike - description: Fetch a "PostLike" resource - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-PostLike-put - description: Update a "PostLike" resource - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-PostLike-patch - description: Update a "PostLike" resource - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - delete: - operationId: delete-PostLike - description: Delete a "PostLike" resource - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}/post: - get: - operationId: fetch-PostLike-related-post - description: Fetch the related "post" resource for "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}/relationships/post: - get: - operationId: fetch-PostLike-relationship-post - description: Fetch the "post" relationships for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-PostLike-relationship-post-put - description: Update "post" relationship for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-PostLike-relationship-post-patch - description: Update "post" relationship for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}/user: - get: - operationId: fetch-PostLike-related-user - description: Fetch the related "user" resource for "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}/relationships/user: - get: - operationId: fetch-PostLike-relationship-user - description: Fetch the "user" relationships for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-PostLike-relationship-user-put - description: Update "user" relationship for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-PostLike-relationship-user-patch - description: Update "user" relationship for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' -components: - schemas: - _jsonapi: - type: object - description: An object describing the server’s implementation - required: - - version - properties: - version: - type: string - _meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Superjson serialization metadata - additionalProperties: true - _resourceIdentifier: - type: object - description: Identifier for a resource - required: - - type - - id - properties: - type: - type: string - description: Resource type - id: - type: string - description: Resource id - _resource: - allOf: - - $ref: '#/components/schemas/_resourceIdentifier' - - type: object - description: A resource with attributes and relationships - properties: - attributes: - type: object - description: Resource attributes - relationships: - type: object - description: Resource relationships - _links: - type: object - required: - - self - description: Links related to the resource - properties: - self: - type: string - description: Link for refetching the curent results - _pagination: - type: object - description: Pagination information - required: - - first - - last - - prev - - next - properties: - first: - type: string - description: Link to the first page - nullable: true - last: - type: string - description: Link to the last page - nullable: true - prev: - type: string - description: Link to the previous page - nullable: true - next: - type: string - description: Link to the next page - nullable: true - _errors: - type: array - description: An array of error objects - items: - type: object - required: - - status - - code - properties: - status: - type: string - description: HTTP status - code: - type: string - description: Error code - prismaCode: - type: string - description: Prisma error code if the error is thrown by Prisma - title: - type: string - description: Error title - detail: - type: string - description: Error detail - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - _errorResponse: - type: object - required: - - errors - description: An error response - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - errors: - $ref: '#/components/schemas/_errors' - _relationLinks: - type: object - required: - - self - - related - description: Links related to a relationship - properties: - self: - type: string - description: Link for fetching this relationship - related: - type: string - description: Link for fetching the resource represented by this relationship - _toOneRelationship: - type: object - description: A to-one relationship - properties: - data: - allOf: - - $ref: '#/components/schemas/_resourceIdentifier' - nullable: true - _toOneRelationshipWithLinks: - type: object - required: - - links - - data - description: A to-one relationship with links - properties: - links: - $ref: '#/components/schemas/_relationLinks' - data: - allOf: - - $ref: '#/components/schemas/_resourceIdentifier' - nullable: true - _toManyRelationship: - type: object - required: - - data - description: A to-many relationship - properties: - data: - type: array - items: - $ref: '#/components/schemas/_resourceIdentifier' - _toManyRelationshipWithLinks: - type: object - required: - - links - - data - description: A to-many relationship with links - properties: - links: - $ref: '#/components/schemas/_pagedRelationLinks' - data: - type: array - items: - $ref: '#/components/schemas/_resourceIdentifier' - _pagedRelationLinks: - description: Relationship links with pagination information - allOf: - - $ref: '#/components/schemas/_pagination' - - $ref: '#/components/schemas/_relationLinks' - _toManyRelationshipRequest: - type: object - required: - - data - description: Input for manipulating a to-many relationship - properties: - data: - type: array - items: - $ref: '#/components/schemas/_resourceIdentifier' - _toOneRelationshipRequest: - description: Input for manipulating a to-one relationship - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/_resourceIdentifier' - nullable: true - _toManyRelationshipResponse: - description: Response for a to-many relationship - allOf: - - $ref: '#/components/schemas/_toManyRelationshipWithLinks' - - type: object - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - _toOneRelationshipResponse: - description: Response for a to-one relationship - allOf: - - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - - type: object - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - role: - type: string - description: The "role" Enum - enum: - - USER - - ADMIN - User: - type: object - description: The "User" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/role' - required: - - createdAt - - updatedAt - - email - - role - relationships: - type: object - properties: - posts: - $ref: '#/components/schemas/_toManyRelationshipWithLinks' - profile: - allOf: - - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - nullable: true - UserCreateRequest: - type: object - description: Input for creating a "User" - required: - - data - properties: - data: - type: object - description: The "User" model - required: - - type - - attributes - properties: - type: - type: string - attributes: - type: object - required: - - updatedAt - - email - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/role' - relationships: - type: object - properties: - posts: - $ref: '#/components/schemas/_toManyRelationship' - profile: - allOf: - - $ref: '#/components/schemas/_toOneRelationship' - nullable: true - meta: - $ref: '#/components/schemas/_meta' - UserUpdateRequest: - type: object - description: Input for updating a "User" - required: - - data - properties: - data: - type: object - description: The "User" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/role' - relationships: - type: object - properties: - posts: - $ref: '#/components/schemas/_toManyRelationship' - profile: - allOf: - - $ref: '#/components/schemas/_toOneRelationship' - nullable: true - meta: - $ref: '#/components/schemas/_meta' - UserResponse: - type: object - description: Response for a "User" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/User' - - type: object - properties: - relationships: - type: object - properties: &a1 - posts: - $ref: '#/components/schemas/_toManyRelationship' - profile: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - UserListResponse: - type: object - description: Response for a list of "User" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/User' - - type: object - properties: - relationships: - type: object - properties: *a1 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - Profile: - type: object - description: The "Profile" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - image: - type: string - nullable: true - userId: - type: string - required: - - image - - userId - relationships: - type: object - properties: - user: - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - ProfileCreateRequest: - type: object - description: Input for creating a "Profile" - required: - - data - properties: - data: - type: object - description: The "Profile" model - required: - - type - - attributes - properties: - type: - type: string - attributes: - type: object - properties: - image: - type: string - nullable: true - relationships: - type: object - properties: - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - ProfileUpdateRequest: - type: object - description: Input for updating a "Profile" - required: - - data - properties: - data: - type: object - description: The "Profile" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - image: - type: string - nullable: true - relationships: - type: object - properties: - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - ProfileResponse: - type: object - description: Response for a "Profile" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/Profile' - - type: object - properties: - relationships: - type: object - properties: &a2 - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - ProfileListResponse: - type: object - description: Response for a list of "Profile" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/Profile' - - type: object - properties: - relationships: - type: object - properties: *a2 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - post_Item: - type: object - description: The "post_Item" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - authorId: - type: string - nullable: true - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - required: - - createdAt - - updatedAt - - title - - authorId - - published - - viewCount - - notes - relationships: - type: object - properties: - author: - allOf: - - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - nullable: true - post_ItemCreateRequest: - type: object - description: Input for creating a "post_Item" - required: - - data - properties: - data: - type: object - description: The "post_Item" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - required: - - updatedAt - - title - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - relationships: - type: object - properties: - author: - allOf: - - $ref: '#/components/schemas/_toOneRelationship' - nullable: true - meta: - $ref: '#/components/schemas/_meta' - post_ItemUpdateRequest: - type: object - description: Input for updating a "post_Item" - required: - - data - properties: - data: - type: object - description: The "post_Item" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - relationships: - type: object - properties: - author: - allOf: - - $ref: '#/components/schemas/_toOneRelationship' - nullable: true - meta: - $ref: '#/components/schemas/_meta' - post_ItemResponse: - type: object - description: Response for a "post_Item" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/post_Item' - - type: object - properties: - relationships: - type: object - properties: &a3 - author: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - post_ItemListResponse: - type: object - description: Response for a list of "post_Item" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/post_Item' - - type: object - properties: - relationships: - type: object - properties: *a3 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - PostLike: - type: object - description: The "PostLike" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - postId: - type: string - userId: - type: string - required: - - postId - - userId - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - user: - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - PostLikeCreateRequest: - type: object - description: Input for creating a "PostLike" - required: - - data - properties: - data: - type: object - description: The "PostLike" model - required: - - type - - attributes - properties: - type: - type: string - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationship' - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - PostLikeUpdateRequest: - type: object - description: Input for updating a "PostLike" - required: - - data - properties: - data: - type: object - description: The "PostLike" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationship' - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - PostLikeResponse: - type: object - description: Response for a "PostLike" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/PostLike' - - type: object - properties: - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationship' - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - PostLikeListResponse: - type: object - description: Response for a list of "PostLike" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/PostLike' - - type: object - properties: - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationship' - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - parameters: - id: - name: id - in: path - description: The resource id - required: true - schema: - type: string - include: - name: include - in: query - description: Relationships to include - required: false - style: form - schema: - type: string - sort: - name: sort - in: query - description: Fields to sort by - required: false - style: form - schema: - type: string - page-offset: - name: page[offset] - in: query - description: Offset for pagination - required: false - style: form - schema: - type: integer - page-limit: - name: page[limit] - in: query - description: Limit for pagination - required: false - style: form - schema: - type: integer diff --git a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml deleted file mode 100644 index 1e331ab8e..000000000 --- a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml +++ /dev/null @@ -1,3279 +0,0 @@ -openapi: 3.1.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: user - description: User operations - - name: profile - description: Profile operations - - name: post_Item - description: Post-related operations - - name: postLike - description: PostLike operations -paths: - /user: - get: - operationId: list-User - description: List "User" resources - tags: - - user - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[createdAt] - required: false - description: Equality filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lt] - required: false - description: Less-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lte] - required: false - description: Less-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gt] - required: false - description: Greater-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gte] - required: false - description: Greater-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt] - required: false - description: Equality filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lt] - required: false - description: Less-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lte] - required: false - description: Less-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gt] - required: false - description: Greater-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gte] - required: false - description: Greater-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[email] - required: false - description: Equality filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$contains] - required: false - description: String contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$icontains] - required: false - description: String case-insensitive contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$search] - required: false - description: String full-text search filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$startsWith] - required: false - description: String startsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$endsWith] - required: false - description: String endsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[role] - required: false - description: Equality filter for "role" - in: query - style: form - explode: false - schema: - $ref: '#/components/schemas/role' - - name: filter[posts] - required: false - description: Equality filter for "posts" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - - name: filter[profile] - required: false - description: Equality filter for "profile" - in: query - style: form - explode: false - schema: - type: string - - name: filter[likes] - required: false - description: Equality filter for "likes" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-User - description: Create a "User" resource - tags: - - user - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}: - get: - operationId: fetch-User - description: Fetch a "User" resource - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-User-put - description: Update a "User" resource - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-User-patch - description: Update a "User" resource - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - delete: - operationId: delete-User - description: Delete a "User" resource - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}/posts: - get: - operationId: fetch-User-related-posts - description: Fetch the related "posts" resource for "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[createdAt] - required: false - description: Equality filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lt] - required: false - description: Less-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lte] - required: false - description: Less-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gt] - required: false - description: Greater-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gte] - required: false - description: Greater-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt] - required: false - description: Equality filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lt] - required: false - description: Less-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lte] - required: false - description: Less-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gt] - required: false - description: Greater-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gte] - required: false - description: Greater-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[email] - required: false - description: Equality filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$contains] - required: false - description: String contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$icontains] - required: false - description: String case-insensitive contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$search] - required: false - description: String full-text search filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$startsWith] - required: false - description: String startsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$endsWith] - required: false - description: String endsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[role] - required: false - description: Equality filter for "role" - in: query - style: form - explode: false - schema: - $ref: '#/components/schemas/role' - - name: filter[posts] - required: false - description: Equality filter for "posts" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - - name: filter[profile] - required: false - description: Equality filter for "profile" - in: query - style: form - explode: false - schema: - type: string - - name: filter[likes] - required: false - description: Equality filter for "likes" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}/relationships/posts: - get: - operationId: fetch-User-relationship-posts - description: Fetch the "posts" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[createdAt] - required: false - description: Equality filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lt] - required: false - description: Less-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lte] - required: false - description: Less-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gt] - required: false - description: Greater-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gte] - required: false - description: Greater-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt] - required: false - description: Equality filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lt] - required: false - description: Less-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lte] - required: false - description: Less-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gt] - required: false - description: Greater-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gte] - required: false - description: Greater-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[email] - required: false - description: Equality filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$contains] - required: false - description: String contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$icontains] - required: false - description: String case-insensitive contains filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$search] - required: false - description: String full-text search filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$startsWith] - required: false - description: String startsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[email$endsWith] - required: false - description: String endsWith filter for "email" - in: query - style: form - explode: false - schema: - type: string - - name: filter[role] - required: false - description: Equality filter for "role" - in: query - style: form - explode: false - schema: - $ref: '#/components/schemas/role' - - name: filter[posts] - required: false - description: Equality filter for "posts" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - - name: filter[profile] - required: false - description: Equality filter for "profile" - in: query - style: form - explode: false - schema: - type: string - - name: filter[likes] - required: false - description: Equality filter for "likes" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-User-relationship-posts-put - description: Update "posts" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-User-relationship-posts-patch - description: Update "posts" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-User-relationship-posts - description: Create new "posts" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toManyRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}/profile: - get: - operationId: fetch-User-related-profile - description: Fetch the related "profile" resource for "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /user/{id}/relationships/profile: - get: - operationId: fetch-User-relationship-profile - description: Fetch the "profile" relationships for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-User-relationship-profile-put - description: Update "profile" relationship for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-User-relationship-profile-patch - description: Update "profile" relationship for a "User" - tags: - - user - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /profile: - get: - operationId: list-Profile - description: List "Profile" resources - tags: - - profile - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[image] - required: false - description: Equality filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$contains] - required: false - description: String contains filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$icontains] - required: false - description: String case-insensitive contains filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$search] - required: false - description: String full-text search filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$startsWith] - required: false - description: String startsWith filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[image$endsWith] - required: false - description: String endsWith filter for "image" - in: query - style: form - explode: false - schema: - type: string - - name: filter[user] - required: false - description: Equality filter for "user" - in: query - style: form - explode: false - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-Profile - description: Create a "Profile" resource - tags: - - profile - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /profile/{id}: - get: - operationId: fetch-Profile - description: Fetch a "Profile" resource - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-Profile-put - description: Update a "Profile" resource - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-Profile-patch - description: Update a "Profile" resource - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/ProfileResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - delete: - operationId: delete-Profile - description: Delete a "Profile" resource - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /profile/{id}/user: - get: - operationId: fetch-Profile-related-user - description: Fetch the related "user" resource for "Profile" - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /profile/{id}/relationships/user: - get: - operationId: fetch-Profile-relationship-user - description: Fetch the "user" relationships for a "Profile" - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-Profile-relationship-user-put - description: Update "user" relationship for a "Profile" - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-Profile-relationship-user-patch - description: Update "user" relationship for a "Profile" - tags: - - profile - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /post_Item: - get: - operationId: list-post_Item - description: List "post_Item" resources - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[createdAt] - required: false - description: Equality filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lt] - required: false - description: Less-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$lte] - required: false - description: Less-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gt] - required: false - description: Greater-than filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[createdAt$gte] - required: false - description: Greater-than or equal filter for "createdAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt] - required: false - description: Equality filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lt] - required: false - description: Less-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$lte] - required: false - description: Less-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gt] - required: false - description: Greater-than filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[updatedAt$gte] - required: false - description: Greater-than or equal filter for "updatedAt" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[title] - required: false - description: Equality filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$contains] - required: false - description: String contains filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$icontains] - required: false - description: String case-insensitive contains filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$search] - required: false - description: String full-text search filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$startsWith] - required: false - description: String startsWith filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[title$endsWith] - required: false - description: String endsWith filter for "title" - in: query - style: form - explode: false - schema: - type: string - - name: filter[author] - required: false - description: Equality filter for "author" - in: query - style: form - explode: false - schema: - type: string - - name: filter[published] - required: false - description: Equality filter for "published" - in: query - style: form - explode: false - schema: - type: boolean - - name: filter[viewCount] - required: false - description: Equality filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[viewCount$lt] - required: false - description: Less-than filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[viewCount$lte] - required: false - description: Less-than or equal filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[viewCount$gt] - required: false - description: Greater-than filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[viewCount$gte] - required: false - description: Greater-than or equal filter for "viewCount" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[notes] - required: false - description: Equality filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$contains] - required: false - description: String contains filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$icontains] - required: false - description: String case-insensitive contains filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$search] - required: false - description: String full-text search filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$startsWith] - required: false - description: String startsWith filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[notes$endsWith] - required: false - description: String endsWith filter for "notes" - in: query - style: form - explode: false - schema: - type: string - - name: filter[likes] - required: false - description: Equality filter for "likes" - in: query - style: form - explode: false - schema: - type: array - items: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-post_Item - description: Create a "post_Item" resource - tags: - - post_Item - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /post_Item/{id}: - get: - operationId: fetch-post_Item - description: Fetch a "post_Item" resource - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-post_Item-put - description: Update a "post_Item" resource - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-post_Item-patch - description: Update a "post_Item" resource - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - delete: - operationId: delete-post_Item - description: Delete a "post_Item" resource - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /post_Item/{id}/author: - get: - operationId: fetch-post_Item-related-author - description: Fetch the related "author" resource for "post_Item" - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /post_Item/{id}/relationships/author: - get: - operationId: fetch-post_Item-relationship-author - description: Fetch the "author" relationships for a "post_Item" - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-post_Item-relationship-author-put - description: Update "author" relationship for a "post_Item" - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-post_Item-relationship-author-patch - description: Update "author" relationship for a "post_Item" - tags: - - post_Item - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike: - get: - operationId: list-PostLike - description: List "PostLike" resources - tags: - - postLike - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[post] - required: false - description: Equality filter for "post" - in: query - style: form - explode: false - schema: - type: string - - name: filter[user] - required: false - description: Equality filter for "user" - in: query - style: form - explode: false - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - post: - operationId: create-PostLike - description: Create a "PostLike" resource - tags: - - postLike - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}: - get: - operationId: fetch-PostLike - description: Fetch a "PostLike" resource - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-PostLike-put - description: Update a "PostLike" resource - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-PostLike-patch - description: Update a "PostLike" resource - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/PostLikeResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - delete: - operationId: delete-PostLike - description: Delete a "PostLike" resource - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}/post: - get: - operationId: fetch-PostLike-related-post - description: Fetch the related "post" resource for "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/post_ItemResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}/relationships/post: - get: - operationId: fetch-PostLike-relationship-post - description: Fetch the "post" relationships for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-PostLike-relationship-post-put - description: Update "post" relationship for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-PostLike-relationship-post-patch - description: Update "post" relationship for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}/user: - get: - operationId: fetch-PostLike-related-user - description: Fetch the related "user" resource for "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/UserResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - /postLike/{id}/relationships/user: - get: - operationId: fetch-PostLike-relationship-user - description: Fetch the "user" relationships for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - put: - operationId: update-PostLike-relationship-user-put - description: Update "user" relationship for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - patch: - operationId: update-PostLike-relationship-user-patch - description: Update "user" relationship for a "PostLike" - tags: - - postLike - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_toOneRelationshipResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' -components: - schemas: - _jsonapi: - type: object - description: An object describing the server’s implementation - required: - - version - properties: - version: - type: string - _meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Superjson serialization metadata - additionalProperties: true - _resourceIdentifier: - type: object - description: Identifier for a resource - required: - - type - - id - properties: - type: - type: string - description: Resource type - id: - type: string - description: Resource id - _resource: - allOf: - - $ref: '#/components/schemas/_resourceIdentifier' - - type: object - description: A resource with attributes and relationships - properties: - attributes: - type: object - description: Resource attributes - relationships: - type: object - description: Resource relationships - _links: - type: object - required: - - self - description: Links related to the resource - properties: - self: - type: string - description: Link for refetching the curent results - _pagination: - type: object - description: Pagination information - required: - - first - - last - - prev - - next - properties: - first: - oneOf: - - type: 'null' - - type: string - description: Link to the first page - last: - oneOf: - - type: 'null' - - type: string - description: Link to the last page - prev: - oneOf: - - type: 'null' - - type: string - description: Link to the previous page - next: - oneOf: - - type: 'null' - - type: string - description: Link to the next page - _errors: - type: array - description: An array of error objects - items: - type: object - required: - - status - - code - properties: - status: - type: string - description: HTTP status - code: - type: string - description: Error code - prismaCode: - type: string - description: Prisma error code if the error is thrown by Prisma - title: - type: string - description: Error title - detail: - type: string - description: Error detail - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - _errorResponse: - type: object - required: - - errors - description: An error response - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - errors: - $ref: '#/components/schemas/_errors' - _relationLinks: - type: object - required: - - self - - related - description: Links related to a relationship - properties: - self: - type: string - description: Link for fetching this relationship - related: - type: string - description: Link for fetching the resource represented by this relationship - _toOneRelationship: - type: object - description: A to-one relationship - properties: - data: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/_resourceIdentifier' - _toOneRelationshipWithLinks: - type: object - required: - - links - - data - description: A to-one relationship with links - properties: - links: - $ref: '#/components/schemas/_relationLinks' - data: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/_resourceIdentifier' - _toManyRelationship: - type: object - required: - - data - description: A to-many relationship - properties: - data: - type: array - items: - $ref: '#/components/schemas/_resourceIdentifier' - _toManyRelationshipWithLinks: - type: object - required: - - links - - data - description: A to-many relationship with links - properties: - links: - $ref: '#/components/schemas/_pagedRelationLinks' - data: - type: array - items: - $ref: '#/components/schemas/_resourceIdentifier' - _pagedRelationLinks: - description: Relationship links with pagination information - allOf: - - $ref: '#/components/schemas/_pagination' - - $ref: '#/components/schemas/_relationLinks' - _toManyRelationshipRequest: - type: object - required: - - data - description: Input for manipulating a to-many relationship - properties: - data: - type: array - items: - $ref: '#/components/schemas/_resourceIdentifier' - _toOneRelationshipRequest: - description: Input for manipulating a to-one relationship - oneOf: - - type: 'null' - - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/_resourceIdentifier' - _toManyRelationshipResponse: - description: Response for a to-many relationship - allOf: - - $ref: '#/components/schemas/_toManyRelationshipWithLinks' - - type: object - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - _toOneRelationshipResponse: - description: Response for a to-one relationship - allOf: - - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - - type: object - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - role: - type: string - description: The "role" Enum - enum: - - USER - - ADMIN - User: - type: object - description: The "User" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/role' - required: - - createdAt - - updatedAt - - email - - role - relationships: - type: object - properties: - posts: - $ref: '#/components/schemas/_toManyRelationshipWithLinks' - profile: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - UserCreateRequest: - type: object - description: Input for creating a "User" - required: - - data - properties: - data: - type: object - description: The "User" model - required: - - type - - attributes - properties: - type: - type: string - attributes: - type: object - required: - - updatedAt - - email - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/role' - relationships: - type: object - properties: - posts: - $ref: '#/components/schemas/_toManyRelationship' - profile: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - UserUpdateRequest: - type: object - description: Input for updating a "User" - required: - - data - properties: - data: - type: object - description: The "User" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/role' - relationships: - type: object - properties: - posts: - $ref: '#/components/schemas/_toManyRelationship' - profile: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - UserResponse: - type: object - description: Response for a "User" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/User' - - type: object - properties: - relationships: - type: object - properties: &a1 - posts: - $ref: '#/components/schemas/_toManyRelationship' - profile: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - UserListResponse: - type: object - description: Response for a list of "User" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/User' - - type: object - properties: - relationships: - type: object - properties: *a1 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - Profile: - type: object - description: The "Profile" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - image: - oneOf: - - type: 'null' - - type: string - userId: - type: string - required: - - image - - userId - relationships: - type: object - properties: - user: - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - ProfileCreateRequest: - type: object - description: Input for creating a "Profile" - required: - - data - properties: - data: - type: object - description: The "Profile" model - required: - - type - - attributes - properties: - type: - type: string - attributes: - type: object - properties: - image: - oneOf: - - type: 'null' - - type: string - relationships: - type: object - properties: - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - ProfileUpdateRequest: - type: object - description: Input for updating a "Profile" - required: - - data - properties: - data: - type: object - description: The "Profile" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - image: - oneOf: - - type: 'null' - - type: string - relationships: - type: object - properties: - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - ProfileResponse: - type: object - description: Response for a "Profile" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/Profile' - - type: object - properties: - relationships: - type: object - properties: &a2 - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - ProfileListResponse: - type: object - description: Response for a list of "Profile" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/Profile' - - type: object - properties: - relationships: - type: object - properties: *a2 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - post_Item: - type: object - description: The "post_Item" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - authorId: - oneOf: - - type: 'null' - - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - required: - - createdAt - - updatedAt - - title - - authorId - - published - - viewCount - - notes - relationships: - type: object - properties: - author: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - post_ItemCreateRequest: - type: object - description: Input for creating a "post_Item" - required: - - data - properties: - data: - type: object - description: The "post_Item" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - required: - - updatedAt - - title - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - relationships: - type: object - properties: - author: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - post_ItemUpdateRequest: - type: object - description: Input for updating a "post_Item" - required: - - data - properties: - data: - type: object - description: The "post_Item" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - relationships: - type: object - properties: - author: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - post_ItemResponse: - type: object - description: Response for a "post_Item" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/post_Item' - - type: object - properties: - relationships: - type: object - properties: &a3 - author: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - post_ItemListResponse: - type: object - description: Response for a list of "post_Item" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/post_Item' - - type: object - properties: - relationships: - type: object - properties: *a3 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - PostLike: - type: object - description: The "PostLike" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - postId: - type: string - userId: - type: string - required: - - postId - - userId - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - user: - $ref: '#/components/schemas/_toOneRelationshipWithLinks' - PostLikeCreateRequest: - type: object - description: Input for creating a "PostLike" - required: - - data - properties: - data: - type: object - description: The "PostLike" model - required: - - type - - attributes - properties: - type: - type: string - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationship' - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - PostLikeUpdateRequest: - type: object - description: Input for updating a "PostLike" - required: - - data - properties: - data: - type: object - description: The "PostLike" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationship' - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - PostLikeResponse: - type: object - description: Response for a "PostLike" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/PostLike' - - type: object - properties: - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationship' - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - PostLikeListResponse: - type: object - description: Response for a list of "PostLike" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/PostLike' - - type: object - properties: - relationships: - type: object - properties: - post: - $ref: '#/components/schemas/_toOneRelationship' - user: - $ref: '#/components/schemas/_toOneRelationship' - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - parameters: - id: - name: id - in: path - description: The resource id - required: true - schema: - type: string - include: - name: include - in: query - description: Relationships to include - required: false - style: form - schema: - type: string - sort: - name: sort - in: query - description: Fields to sort by - required: false - style: form - schema: - type: string - page-offset: - name: page[offset] - in: query - description: Offset for pagination - required: false - style: form - schema: - type: integer - page-limit: - name: page[limit] - in: query - description: Limit for pagination - required: false - style: form - schema: - type: integer diff --git a/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.0.0.baseline.yaml deleted file mode 100644 index 71b0b859d..000000000 --- a/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.0.0.baseline.yaml +++ /dev/null @@ -1,872 +0,0 @@ -openapi: 3.0.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: foo - description: Foo operations -paths: - /foo: - get: - operationId: list-Foo - description: List "Foo" resources - tags: - - foo - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[string] - required: false - description: Equality filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$contains] - required: false - description: String contains filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$icontains] - required: false - description: String case-insensitive contains filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$search] - required: false - description: String full-text search filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$startsWith] - required: false - description: String startsWith filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$endsWith] - required: false - description: String endsWith filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[int] - required: false - description: Equality filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$lt] - required: false - description: Less-than filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$lte] - required: false - description: Less-than or equal filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$gt] - required: false - description: Greater-than filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$gte] - required: false - description: Greater-than or equal filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt] - required: false - description: Equality filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$lt] - required: false - description: Less-than filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$lte] - required: false - description: Less-than or equal filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$gt] - required: false - description: Greater-than filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$gte] - required: false - description: Greater-than or equal filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[date] - required: false - description: Equality filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$lt] - required: false - description: Less-than filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$lte] - required: false - description: Less-than or equal filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$gt] - required: false - description: Greater-than filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$gte] - required: false - description: Greater-than or equal filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[float] - required: false - description: Equality filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$lt] - required: false - description: Less-than filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$lte] - required: false - description: Less-than or equal filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$gt] - required: false - description: Greater-than filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$gte] - required: false - description: Greater-than or equal filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[decimal] - required: false - description: Equality filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$lt] - required: false - description: Less-than filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$lte] - required: false - description: Less-than or equal filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$gt] - required: false - description: Greater-than filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$gte] - required: false - description: Greater-than or equal filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[boolean] - required: false - description: Equality filter for "boolean" - in: query - style: form - explode: false - schema: - type: boolean - - name: filter[bytes] - required: false - description: Equality filter for "bytes" - in: query - style: form - explode: false - schema: - type: string - format: byte - description: Base64 encoded byte array - - name: filter[json] - required: false - description: Equality filter for "json" - in: query - style: form - explode: false - schema: - type: string - format: json - - name: filter[plainJson] - required: false - description: Equality filter for "plainJson" - in: query - style: form - explode: false - schema: - type: string - format: json - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - post: - operationId: create-Foo - description: Create a "Foo" resource - tags: - - foo - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - /foo/{id}: - get: - operationId: fetch-Foo - description: Fetch a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - put: - operationId: update-Foo-put - description: Update a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - patch: - operationId: update-Foo-patch - description: Update a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - delete: - operationId: delete-Foo - description: Delete a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] -components: - schemas: - _jsonapi: - type: object - description: An object describing the server’s implementation - required: - - version - properties: - version: - type: string - _meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Superjson serialization metadata - additionalProperties: true - _resourceIdentifier: - type: object - description: Identifier for a resource - required: - - type - - id - properties: - type: - type: string - description: Resource type - id: - type: string - description: Resource id - _resource: - allOf: - - $ref: '#/components/schemas/_resourceIdentifier' - - type: object - description: A resource with attributes and relationships - properties: - attributes: - type: object - description: Resource attributes - relationships: - type: object - description: Resource relationships - _links: - type: object - required: - - self - description: Links related to the resource - properties: - self: - type: string - description: Link for refetching the curent results - _pagination: - type: object - description: Pagination information - required: - - first - - last - - prev - - next - properties: - first: - type: string - description: Link to the first page - nullable: true - last: - type: string - description: Link to the last page - nullable: true - prev: - type: string - description: Link to the previous page - nullable: true - next: - type: string - description: Link to the next page - nullable: true - _errors: - type: array - description: An array of error objects - items: - type: object - required: - - status - - code - properties: - status: - type: string - description: HTTP status - code: - type: string - description: Error code - prismaCode: - type: string - description: Prisma error code if the error is thrown by Prisma - title: - type: string - description: Error title - detail: - type: string - description: Error detail - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - _errorResponse: - type: object - required: - - errors - description: An error response - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - errors: - $ref: '#/components/schemas/_errors' - Foo: - type: object - description: The "Foo" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - - json - - plainJson - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - json: - allOf: - - $ref: '#/components/schemas/Meta' - nullable: true - plainJson: {} - FooCreateRequest: - type: object - description: Input for creating a "Foo" - required: - - data - properties: - data: - type: object - description: The "Foo" model - required: - - type - - attributes - properties: - type: - type: string - attributes: - type: object - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - - plainJson - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - json: - allOf: - - $ref: '#/components/schemas/Meta' - nullable: true - plainJson: {} - meta: - $ref: '#/components/schemas/_meta' - FooUpdateRequest: - type: object - description: Input for updating a "Foo" - required: - - data - properties: - data: - type: object - description: The "Foo" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - json: - allOf: - - $ref: '#/components/schemas/Meta' - nullable: true - plainJson: {} - meta: - $ref: '#/components/schemas/_meta' - FooResponse: - type: object - description: Response for a "Foo" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/Foo' - - type: object - properties: - relationships: - type: object - properties: &a1 {} - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - FooListResponse: - type: object - description: Response for a list of "Foo" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/Foo' - - type: object - properties: - relationships: - type: object - properties: *a1 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - Meta: - type: object - description: The "Meta" TypeDef - properties: - something: - type: string - parameters: - id: - name: id - in: path - description: The resource id - required: true - schema: - type: string - include: - name: include - in: query - description: Relationships to include - required: false - style: form - schema: - type: string - sort: - name: sort - in: query - description: Fields to sort by - required: false - style: form - schema: - type: string - page-offset: - name: page[offset] - in: query - description: Offset for pagination - required: false - style: form - schema: - type: integer - page-limit: - name: page[limit] - in: query - description: Limit for pagination - required: false - style: form - schema: - type: integer diff --git a/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.1.0.baseline.yaml deleted file mode 100644 index 6d41ebdf6..000000000 --- a/packages/plugins/openapi/tests/baseline/rest-type-coverage-3.1.0.baseline.yaml +++ /dev/null @@ -1,876 +0,0 @@ -openapi: 3.1.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: foo - description: Foo operations -paths: - /foo: - get: - operationId: list-Foo - description: List "Foo" resources - tags: - - foo - parameters: - - $ref: '#/components/parameters/include' - - $ref: '#/components/parameters/sort' - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-limit' - - name: filter[id] - required: false - description: Id filter - in: query - style: form - explode: false - schema: - type: string - - name: filter[string] - required: false - description: Equality filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$contains] - required: false - description: String contains filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$icontains] - required: false - description: String case-insensitive contains filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$search] - required: false - description: String full-text search filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$startsWith] - required: false - description: String startsWith filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[string$endsWith] - required: false - description: String endsWith filter for "string" - in: query - style: form - explode: false - schema: - type: string - - name: filter[int] - required: false - description: Equality filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$lt] - required: false - description: Less-than filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$lte] - required: false - description: Less-than or equal filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$gt] - required: false - description: Greater-than filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[int$gte] - required: false - description: Greater-than or equal filter for "int" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt] - required: false - description: Equality filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$lt] - required: false - description: Less-than filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$lte] - required: false - description: Less-than or equal filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$gt] - required: false - description: Greater-than filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[bigInt$gte] - required: false - description: Greater-than or equal filter for "bigInt" - in: query - style: form - explode: false - schema: - type: integer - - name: filter[date] - required: false - description: Equality filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$lt] - required: false - description: Less-than filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$lte] - required: false - description: Less-than or equal filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$gt] - required: false - description: Greater-than filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[date$gte] - required: false - description: Greater-than or equal filter for "date" - in: query - style: form - explode: false - schema: - type: string - format: date-time - - name: filter[float] - required: false - description: Equality filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$lt] - required: false - description: Less-than filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$lte] - required: false - description: Less-than or equal filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$gt] - required: false - description: Greater-than filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[float$gte] - required: false - description: Greater-than or equal filter for "float" - in: query - style: form - explode: false - schema: - type: number - - name: filter[decimal] - required: false - description: Equality filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$lt] - required: false - description: Less-than filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$lte] - required: false - description: Less-than or equal filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$gt] - required: false - description: Greater-than filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[decimal$gte] - required: false - description: Greater-than or equal filter for "decimal" - in: query - style: form - explode: false - schema: - oneOf: - - type: number - - type: string - - name: filter[boolean] - required: false - description: Equality filter for "boolean" - in: query - style: form - explode: false - schema: - type: boolean - - name: filter[bytes] - required: false - description: Equality filter for "bytes" - in: query - style: form - explode: false - schema: - type: string - format: byte - description: Base64 encoded byte array - - name: filter[json] - required: false - description: Equality filter for "json" - in: query - style: form - explode: false - schema: - type: string - format: json - - name: filter[plainJson] - required: false - description: Equality filter for "plainJson" - in: query - style: form - explode: false - schema: - type: string - format: json - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooListResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - post: - operationId: create-Foo - description: Create a "Foo" resource - tags: - - foo - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooCreateRequest' - responses: - '201': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - /foo/{id}: - get: - operationId: fetch-Foo - description: Fetch a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - - $ref: '#/components/parameters/include' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - put: - operationId: update-Foo-put - description: Update a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - patch: - operationId: update-Foo-patch - description: Update a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - requestBody: - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooUpdateRequest' - responses: - '200': - description: Successful operation - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/FooResponse' - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '422': - description: Request is unprocessable due to validation errors - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] - delete: - operationId: delete-Foo - description: Delete a "Foo" resource - tags: - - foo - parameters: - - $ref: '#/components/parameters/id' - responses: - '200': - description: Successful operation - '403': - description: Request is forbidden - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - '404': - description: Resource is not found - content: - application/vnd.api+json: - schema: - $ref: '#/components/schemas/_errorResponse' - security: [] -components: - schemas: - _jsonapi: - type: object - description: An object describing the server’s implementation - required: - - version - properties: - version: - type: string - _meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Superjson serialization metadata - additionalProperties: true - _resourceIdentifier: - type: object - description: Identifier for a resource - required: - - type - - id - properties: - type: - type: string - description: Resource type - id: - type: string - description: Resource id - _resource: - allOf: - - $ref: '#/components/schemas/_resourceIdentifier' - - type: object - description: A resource with attributes and relationships - properties: - attributes: - type: object - description: Resource attributes - relationships: - type: object - description: Resource relationships - _links: - type: object - required: - - self - description: Links related to the resource - properties: - self: - type: string - description: Link for refetching the curent results - _pagination: - type: object - description: Pagination information - required: - - first - - last - - prev - - next - properties: - first: - oneOf: - - type: 'null' - - type: string - description: Link to the first page - last: - oneOf: - - type: 'null' - - type: string - description: Link to the last page - prev: - oneOf: - - type: 'null' - - type: string - description: Link to the previous page - next: - oneOf: - - type: 'null' - - type: string - description: Link to the next page - _errors: - type: array - description: An array of error objects - items: - type: object - required: - - status - - code - properties: - status: - type: string - description: HTTP status - code: - type: string - description: Error code - prismaCode: - type: string - description: Prisma error code if the error is thrown by Prisma - title: - type: string - description: Error title - detail: - type: string - description: Error detail - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - _errorResponse: - type: object - required: - - errors - description: An error response - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - errors: - $ref: '#/components/schemas/_errors' - Foo: - type: object - description: The "Foo" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - - json - - plainJson - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - json: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Meta' - plainJson: {} - FooCreateRequest: - type: object - description: Input for creating a "Foo" - required: - - data - properties: - data: - type: object - description: The "Foo" model - required: - - type - - attributes - properties: - type: - type: string - attributes: - type: object - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - - plainJson - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - json: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Meta' - plainJson: {} - meta: - $ref: '#/components/schemas/_meta' - FooUpdateRequest: - type: object - description: Input for updating a "Foo" - required: - - data - properties: - data: - type: object - description: The "Foo" model - required: - - id - - type - - attributes - properties: - id: - type: string - type: - type: string - attributes: - type: object - properties: - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: number - - type: string - boolean: - type: boolean - bytes: - type: string - format: byte - description: Base64 encoded byte array - json: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Meta' - plainJson: {} - meta: - $ref: '#/components/schemas/_meta' - FooResponse: - type: object - description: Response for a "Foo" - required: - - data - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - allOf: - - $ref: '#/components/schemas/Foo' - - type: object - properties: - relationships: - type: object - properties: &a1 {} - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - $ref: '#/components/schemas/_links' - FooListResponse: - type: object - description: Response for a list of "Foo" - required: - - data - - links - properties: - jsonapi: - $ref: '#/components/schemas/_jsonapi' - data: - type: array - items: - allOf: - - $ref: '#/components/schemas/Foo' - - type: object - properties: - relationships: - type: object - properties: *a1 - meta: - $ref: '#/components/schemas/_meta' - included: - type: array - items: - $ref: '#/components/schemas/_resource' - links: - allOf: - - $ref: '#/components/schemas/_links' - - $ref: '#/components/schemas/_pagination' - Meta: - type: object - description: The "Meta" TypeDef - properties: - something: - type: string - parameters: - id: - name: id - in: path - description: The resource id - required: true - schema: - type: string - include: - name: include - in: query - description: Relationships to include - required: false - style: form - schema: - type: string - sort: - name: sort - in: query - description: Fields to sort by - required: false - style: form - schema: - type: string - page-offset: - name: page[offset] - in: query - description: Offset for pagination - required: false - style: form - schema: - type: integer - page-limit: - name: page[limit] - in: query - description: Limit for pagination - required: false - style: form - schema: - type: integer diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.0.0-omit.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.0.0-omit.baseline.yaml deleted file mode 100644 index 0a48952db..000000000 --- a/packages/plugins/openapi/tests/baseline/rpc-3.0.0-omit.baseline.yaml +++ /dev/null @@ -1,3020 +0,0 @@ -openapi: 3.0.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: user - description: User operations - - name: profile - description: Profile operations - - name: post_Item - description: Post-related operations -components: - schemas: - _AnyObject: - type: object - additionalProperties: true - Role: - type: string - enum: - - USER - - ADMIN - User: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - type: array - items: - $ref: '#/components/schemas/Post_Item' - profile: - allOf: - - $ref: '#/components/schemas/Profile' - nullable: true - required: - - id - - createdAt - - updatedAt - - email - - role - Profile: - type: object - properties: - id: - type: string - image: - type: string - nullable: true - user: - $ref: '#/components/schemas/User' - userId: - type: string - required: - - id - - user - - userId - Post_Item: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - author: - allOf: - - $ref: '#/components/schemas/User' - nullable: true - authorId: - type: string - nullable: true - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - required: - - id - - createdAt - - updatedAt - - title - - published - - viewCount - AggregateUser: - type: object - properties: - _count: - allOf: - - $ref: '#/components/schemas/UserCountAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/UserMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/UserMaxAggregateOutputType' - nullable: true - UserGroupByOutputType: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - _count: - allOf: - - $ref: '#/components/schemas/UserCountAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/UserMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/UserMaxAggregateOutputType' - nullable: true - required: - - id - - createdAt - - updatedAt - - email - - role - AggregateProfile: - type: object - properties: - _count: - allOf: - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/ProfileMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' - nullable: true - ProfileGroupByOutputType: - type: object - properties: - id: - type: string - image: - type: string - nullable: true - userId: - type: string - _count: - allOf: - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/ProfileMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' - nullable: true - required: - - id - - userId - AggregatePost_Item: - type: object - properties: - _count: - allOf: - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - nullable: true - _avg: - allOf: - - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' - nullable: true - _sum: - allOf: - - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' - nullable: true - Post_ItemGroupByOutputType: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - authorId: - type: string - nullable: true - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - _count: - allOf: - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - nullable: true - _avg: - allOf: - - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' - nullable: true - _sum: - allOf: - - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' - nullable: true - required: - - id - - createdAt - - updatedAt - - title - - published - - viewCount - UserCountAggregateOutputType: - type: object - properties: - id: - type: integer - createdAt: - type: integer - updatedAt: - type: integer - email: - type: integer - role: - type: integer - _all: - type: integer - required: - - id - - createdAt - - updatedAt - - email - - role - - _all - UserMinAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - createdAt: - type: string - format: date-time - nullable: true - updatedAt: - type: string - format: date-time - nullable: true - email: - type: string - nullable: true - role: - allOf: - - $ref: '#/components/schemas/Role' - nullable: true - UserMaxAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - createdAt: - type: string - format: date-time - nullable: true - updatedAt: - type: string - format: date-time - nullable: true - email: - type: string - nullable: true - role: - allOf: - - $ref: '#/components/schemas/Role' - nullable: true - ProfileCountAggregateOutputType: - type: object - properties: - id: - type: integer - image: - type: integer - userId: - type: integer - _all: - type: integer - required: - - id - - image - - userId - - _all - ProfileMinAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - image: - type: string - nullable: true - userId: - type: string - nullable: true - ProfileMaxAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - image: - type: string - nullable: true - userId: - type: string - nullable: true - Post_ItemCountAggregateOutputType: - type: object - properties: - id: - type: integer - createdAt: - type: integer - updatedAt: - type: integer - title: - type: integer - authorId: - type: integer - published: - type: integer - viewCount: - type: integer - notes: - type: integer - _all: - type: integer - required: - - id - - createdAt - - updatedAt - - title - - authorId - - published - - viewCount - - notes - - _all - Post_ItemAvgAggregateOutputType: - type: object - properties: - viewCount: - type: number - nullable: true - Post_ItemSumAggregateOutputType: - type: object - properties: - viewCount: - type: integer - nullable: true - Post_ItemMinAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - createdAt: - type: string - format: date-time - nullable: true - updatedAt: - type: string - format: date-time - nullable: true - title: - type: string - nullable: true - authorId: - type: string - nullable: true - published: - type: boolean - nullable: true - viewCount: - type: integer - nullable: true - notes: - type: string - nullable: true - Post_ItemMaxAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - createdAt: - type: string - format: date-time - nullable: true - updatedAt: - type: string - format: date-time - nullable: true - title: - type: string - nullable: true - authorId: - type: string - nullable: true - published: - type: boolean - nullable: true - viewCount: - type: integer - nullable: true - notes: - type: string - nullable: true - _Meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Serialization metadata - additionalProperties: true - _Error: - type: object - required: - - error - properties: - error: - type: object - required: - - message - properties: - prisma: - type: boolean - description: Indicates if the error occurred during a Prisma call - rejectedByPolicy: - type: boolean - description: Indicates if the error was due to rejection by a policy - code: - type: string - description: Prisma error code. Only available when "prisma" field is true. - message: - type: string - description: Error message - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - additionalProperties: true - BatchPayload: - type: object - properties: - count: - type: integer - UserCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - UserFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - UserUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - create: - $ref: '#/components/schemas/_AnyObject' - update: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/_AnyObject' - _min: - $ref: '#/components/schemas/_AnyObject' - _max: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - by: - $ref: '#/components/schemas/_AnyObject' - having: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/_AnyObject' - _min: - $ref: '#/components/schemas/_AnyObject' - _max: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - ProfileUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - create: - $ref: '#/components/schemas/_AnyObject' - update: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/_AnyObject' - _min: - $ref: '#/components/schemas/_AnyObject' - _max: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - by: - $ref: '#/components/schemas/_AnyObject' - having: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/_AnyObject' - _min: - $ref: '#/components/schemas/_AnyObject' - _max: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - create: - $ref: '#/components/schemas/_AnyObject' - update: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - by: - $ref: '#/components/schemas/_AnyObject' - having: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' -paths: - /user/create: - post: - operationId: createUser - description: Create a new User - tags: - - user - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreateArgs' - /user/createMany: - post: - operationId: createManyUser - description: Create several User - tags: - - user - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreateManyArgs' - /user/findUnique: - get: - operationId: findUniqueUser - description: Find one unique User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/findFirst: - get: - operationId: findFirstUser - description: Find the first User matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/findMany: - get: - operationId: findManyUser - description: Find users matching the given conditions - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/update: - patch: - operationId: updateUser - description: Update a User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdateArgs' - /user/updateMany: - patch: - operationId: updateManyUser - description: Update Users matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdateManyArgs' - /user/upsert: - post: - operationId: upsertUser - description: Upsert a User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpsertArgs' - /user/dodelete: - put: - operationId: deleteUser - description: Delete a unique user - tags: - - delete - - user - summary: Delete a user yeah yeah - deprecated: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserDeleteUniqueArgs' - /user/deleteMany: - delete: - operationId: deleteManyUser - description: Delete Users matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/count: - get: - operationId: countUser - description: Find a list of User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/UserCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/aggregate: - get: - operationId: aggregateUser - description: Aggregate Users - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateUser' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/groupBy: - get: - operationId: groupByUser - description: Group Users by fields - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/UserGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/create: - post: - operationId: createProfile - description: Create a new Profile - tags: - - profile - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCreateArgs' - /profile/createMany: - post: - operationId: createManyProfile - description: Create several Profile - tags: - - profile - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCreateManyArgs' - /profile/findUnique: - get: - operationId: findUniqueProfile - description: Find one unique Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/findFirst: - get: - operationId: findFirstProfile - description: Find the first Profile matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/findMany: - get: - operationId: findManyProfile - description: Find a list of Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/update: - patch: - operationId: updateProfile - description: Update a Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpdateArgs' - /profile/updateMany: - patch: - operationId: updateManyProfile - description: Update Profiles matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpdateManyArgs' - /profile/upsert: - post: - operationId: upsertProfile - description: Upsert a Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpsertArgs' - /profile/delete: - delete: - operationId: deleteProfile - description: Delete one unique Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/deleteMany: - delete: - operationId: deleteManyProfile - description: Delete Profiles matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/count: - get: - operationId: countProfile - description: Find a list of Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/aggregate: - get: - operationId: aggregateProfile - description: Aggregate Profiles - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateProfile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/groupBy: - get: - operationId: groupByProfile - description: Group Profiles by fields - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/ProfileGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/create: - post: - operationId: createPost_Item - description: Create a new Post_Item - tags: - - post_Item - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCreateArgs' - /post_Item/createMany: - post: - operationId: createManyPost_Item - description: Create several Post_Item - tags: - - post_Item - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCreateManyArgs' - /post_Item/findUnique: - get: - operationId: findUniquePost_Item - description: Find one unique Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/findFirst: - get: - operationId: findFirstPost_Item - description: Find the first Post_Item matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/update: - patch: - operationId: updatePost_Item - description: Update a Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpdateArgs' - /post_Item/updateMany: - patch: - operationId: updateManyPost_Item - description: Update Post_Items matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpdateManyArgs' - /post_Item/upsert: - post: - operationId: upsertPost_Item - description: Upsert a Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpsertArgs' - /post_Item/delete: - delete: - operationId: deletePost_Item - description: Delete one unique Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/deleteMany: - delete: - operationId: deleteManyPost_Item - description: Delete Post_Items matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/count: - get: - operationId: countPost_Item - description: Find a list of Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/aggregate: - get: - operationId: aggregatePost_Item - description: Aggregate Post_Items - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregatePost_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/groupBy: - get: - operationId: groupByPost_Item - description: Group Post_Items by fields - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Post_ItemGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml deleted file mode 100644 index b3c4deaa8..000000000 --- a/packages/plugins/openapi/tests/baseline/rpc-3.0.0.baseline.yaml +++ /dev/null @@ -1,5789 +0,0 @@ -openapi: 3.0.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: user - description: User operations - - name: profile - description: Profile operations - - name: post_Item - description: Post-related operations -components: - schemas: - Role: - type: string - enum: - - USER - - ADMIN - UserScalarFieldEnum: - type: string - enum: - - id - - createdAt - - updatedAt - - email - - role - ProfileScalarFieldEnum: - type: string - enum: - - id - - image - - userId - Post_ItemScalarFieldEnum: - type: string - enum: - - id - - createdAt - - updatedAt - - title - - authorId - - published - - viewCount - - notes - SortOrder: - type: string - enum: - - asc - - desc - QueryMode: - type: string - enum: - - default - - insensitive - NullsOrder: - type: string - enum: - - first - - last - User: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - type: array - items: - $ref: '#/components/schemas/Post_Item' - profile: - allOf: - - $ref: '#/components/schemas/Profile' - nullable: true - required: - - id - - createdAt - - updatedAt - - email - - role - Profile: - type: object - properties: - id: - type: string - image: - type: string - nullable: true - user: - $ref: '#/components/schemas/User' - userId: - type: string - required: - - id - - user - - userId - Post_Item: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - author: - allOf: - - $ref: '#/components/schemas/User' - nullable: true - authorId: - type: string - nullable: true - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - required: - - id - - createdAt - - updatedAt - - title - - published - - viewCount - UserWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/UserWhereInput' - - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/UserWhereInput' - - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - email: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - role: - oneOf: - - $ref: '#/components/schemas/EnumroleFilter' - - $ref: '#/components/schemas/Role' - posts: - $ref: '#/components/schemas/Post_ItemListRelationFilter' - profile: - oneOf: - - $ref: '#/components/schemas/ProfileNullableScalarRelationFilter' - - $ref: '#/components/schemas/ProfileWhereInput' - nullable: true - UserOrderByWithRelationInput: - type: object - properties: - id: - $ref: '#/components/schemas/SortOrder' - createdAt: - $ref: '#/components/schemas/SortOrder' - updatedAt: - $ref: '#/components/schemas/SortOrder' - email: - $ref: '#/components/schemas/SortOrder' - role: - $ref: '#/components/schemas/SortOrder' - posts: - $ref: '#/components/schemas/Post_ItemOrderByRelationAggregateInput' - profile: - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - UserWhereUniqueInput: - type: object - properties: - id: - type: string - email: - type: string - AND: - oneOf: - - $ref: '#/components/schemas/UserWhereInput' - - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/UserWhereInput' - - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - role: - oneOf: - - $ref: '#/components/schemas/EnumroleFilter' - - $ref: '#/components/schemas/Role' - posts: - $ref: '#/components/schemas/Post_ItemListRelationFilter' - profile: - oneOf: - - $ref: '#/components/schemas/ProfileNullableScalarRelationFilter' - - $ref: '#/components/schemas/ProfileWhereInput' - nullable: true - UserScalarWhereWithAggregatesInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - OR: - type: array - items: - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - NOT: - oneOf: - - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - id: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - email: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - role: - oneOf: - - $ref: '#/components/schemas/EnumroleWithAggregatesFilter' - - $ref: '#/components/schemas/Role' - ProfileWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/ProfileWhereInput' - - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/ProfileWhereInput' - - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - image: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - nullable: true - userId: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - user: - oneOf: - - $ref: '#/components/schemas/UserScalarRelationFilter' - - $ref: '#/components/schemas/UserWhereInput' - ProfileOrderByWithRelationInput: - type: object - properties: - id: - $ref: '#/components/schemas/SortOrder' - image: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - userId: - $ref: '#/components/schemas/SortOrder' - user: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - ProfileWhereUniqueInput: - type: object - properties: - id: - type: string - userId: - type: string - AND: - oneOf: - - $ref: '#/components/schemas/ProfileWhereInput' - - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/ProfileWhereInput' - - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - image: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - nullable: true - user: - oneOf: - - $ref: '#/components/schemas/UserScalarRelationFilter' - - $ref: '#/components/schemas/UserWhereInput' - ProfileScalarWhereWithAggregatesInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - OR: - type: array - items: - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - NOT: - oneOf: - - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - id: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - image: - oneOf: - - $ref: '#/components/schemas/StringNullableWithAggregatesFilter' - - type: string - nullable: true - userId: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - Post_ItemWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - title: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - authorId: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - nullable: true - published: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - viewCount: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - notes: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - nullable: true - author: - oneOf: - - $ref: '#/components/schemas/UserNullableScalarRelationFilter' - - $ref: '#/components/schemas/UserWhereInput' - nullable: true - Post_ItemOrderByWithRelationInput: - type: object - properties: - id: - $ref: '#/components/schemas/SortOrder' - createdAt: - $ref: '#/components/schemas/SortOrder' - updatedAt: - $ref: '#/components/schemas/SortOrder' - title: - $ref: '#/components/schemas/SortOrder' - authorId: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - published: - $ref: '#/components/schemas/SortOrder' - viewCount: - $ref: '#/components/schemas/SortOrder' - notes: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - author: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - Post_ItemWhereUniqueInput: - type: object - properties: - id: - type: string - AND: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - title: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - authorId: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - nullable: true - published: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - viewCount: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - notes: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - nullable: true - author: - oneOf: - - $ref: '#/components/schemas/UserNullableScalarRelationFilter' - - $ref: '#/components/schemas/UserWhereInput' - nullable: true - Post_ItemScalarWhereWithAggregatesInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - OR: - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - NOT: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - id: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - title: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - authorId: - oneOf: - - $ref: '#/components/schemas/StringNullableWithAggregatesFilter' - - type: string - nullable: true - published: - oneOf: - - $ref: '#/components/schemas/BoolWithAggregatesFilter' - - type: boolean - viewCount: - oneOf: - - $ref: '#/components/schemas/IntWithAggregatesFilter' - - type: integer - notes: - oneOf: - - $ref: '#/components/schemas/StringNullableWithAggregatesFilter' - - type: string - nullable: true - UserCreateInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - $ref: '#/components/schemas/Post_ItemCreateNestedManyWithoutAuthorInput' - profile: - $ref: '#/components/schemas/ProfileCreateNestedOneWithoutUserInput' - required: - - email - UserUpdateInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - posts: - $ref: '#/components/schemas/Post_ItemUpdateManyWithoutAuthorNestedInput' - profile: - $ref: '#/components/schemas/ProfileUpdateOneWithoutUserNestedInput' - UserCreateManyInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - required: - - email - UserUpdateManyMutationInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - ProfileCreateInput: - type: object - properties: - id: - type: string - image: - type: string - nullable: true - user: - $ref: '#/components/schemas/UserCreateNestedOneWithoutProfileInput' - required: - - user - ProfileUpdateInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - image: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - user: - $ref: '#/components/schemas/UserUpdateOneRequiredWithoutProfileNestedInput' - ProfileCreateManyInput: - type: object - properties: - id: - type: string - image: - type: string - nullable: true - userId: - type: string - required: - - userId - ProfileUpdateManyMutationInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - image: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - Post_ItemCreateInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - author: - $ref: '#/components/schemas/UserCreateNestedOneWithoutPostsInput' - required: - - id - - title - Post_ItemUpdateInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - author: - $ref: '#/components/schemas/UserUpdateOneWithoutPostsNestedInput' - Post_ItemCreateManyInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - authorId: - type: string - nullable: true - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - required: - - id - - title - Post_ItemUpdateManyMutationInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - StringFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringFilter' - DateTimeFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeFilter' - EnumroleFilter: - type: object - properties: - equals: - $ref: '#/components/schemas/Role' - in: - type: array - items: - $ref: '#/components/schemas/Role' - notIn: - type: array - items: - $ref: '#/components/schemas/Role' - not: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/NestedEnumroleFilter' - Post_ItemListRelationFilter: - type: object - properties: - every: - $ref: '#/components/schemas/Post_ItemWhereInput' - some: - $ref: '#/components/schemas/Post_ItemWhereInput' - none: - $ref: '#/components/schemas/Post_ItemWhereInput' - ProfileNullableScalarRelationFilter: - type: object - properties: - is: - allOf: - - $ref: '#/components/schemas/ProfileWhereInput' - nullable: true - isNot: - allOf: - - $ref: '#/components/schemas/ProfileWhereInput' - nullable: true - Post_ItemOrderByRelationAggregateInput: - type: object - properties: - _count: - $ref: '#/components/schemas/SortOrder' - StringWithAggregatesFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedStringFilter' - _max: - $ref: '#/components/schemas/NestedStringFilter' - DateTimeWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedDateTimeFilter' - _max: - $ref: '#/components/schemas/NestedDateTimeFilter' - EnumroleWithAggregatesFilter: - type: object - properties: - equals: - $ref: '#/components/schemas/Role' - in: - type: array - items: - $ref: '#/components/schemas/Role' - notIn: - type: array - items: - $ref: '#/components/schemas/Role' - not: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/NestedEnumroleWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedEnumroleFilter' - _max: - $ref: '#/components/schemas/NestedEnumroleFilter' - StringNullableFilter: - type: object - properties: - equals: - type: string - nullable: true - in: - type: array - items: - type: string - nullable: true - notIn: - type: array - items: - type: string - nullable: true - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringNullableFilter' - nullable: true - UserScalarRelationFilter: - type: object - properties: - is: - $ref: '#/components/schemas/UserWhereInput' - isNot: - $ref: '#/components/schemas/UserWhereInput' - SortOrderInput: - type: object - properties: - sort: - $ref: '#/components/schemas/SortOrder' - nulls: - $ref: '#/components/schemas/NullsOrder' - required: - - sort - StringNullableWithAggregatesFilter: - type: object - properties: - equals: - type: string - nullable: true - in: - type: array - items: - type: string - nullable: true - notIn: - type: array - items: - type: string - nullable: true - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringNullableWithAggregatesFilter' - nullable: true - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedStringNullableFilter' - _max: - $ref: '#/components/schemas/NestedStringNullableFilter' - BoolFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolFilter' - IntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntFilter' - UserNullableScalarRelationFilter: - type: object - properties: - is: - allOf: - - $ref: '#/components/schemas/UserWhereInput' - nullable: true - isNot: - allOf: - - $ref: '#/components/schemas/UserWhereInput' - nullable: true - BoolWithAggregatesFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedBoolFilter' - _max: - $ref: '#/components/schemas/NestedBoolFilter' - IntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedIntFilter' - _max: - $ref: '#/components/schemas/NestedIntFilter' - Post_ItemCreateNestedManyWithoutAuthorInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - connectOrCreate: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - createMany: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInputEnvelope' - connect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - ProfileCreateNestedOneWithoutUserInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - connectOrCreate: - $ref: '#/components/schemas/ProfileCreateOrConnectWithoutUserInput' - connect: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - Post_ItemUncheckedCreateNestedManyWithoutAuthorInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - connectOrCreate: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - createMany: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInputEnvelope' - connect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - ProfileUncheckedCreateNestedOneWithoutUserInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - connectOrCreate: - $ref: '#/components/schemas/ProfileCreateOrConnectWithoutUserInput' - connect: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - StringFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - DateTimeFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - format: date-time - EnumroleFieldUpdateOperationsInput: - type: object - properties: - set: - $ref: '#/components/schemas/Role' - Post_ItemUpdateManyWithoutAuthorNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - connectOrCreate: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - upsert: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpsertWithWhereUniqueWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpsertWithWhereUniqueWithoutAuthorInput' - createMany: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInputEnvelope' - set: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - disconnect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - delete: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - connect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateWithWhereUniqueWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpdateWithWhereUniqueWithoutAuthorInput' - updateMany: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateManyWithWhereWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpdateManyWithWhereWithoutAuthorInput' - deleteMany: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - ProfileUpdateOneWithoutUserNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - connectOrCreate: - $ref: '#/components/schemas/ProfileCreateOrConnectWithoutUserInput' - upsert: - $ref: '#/components/schemas/ProfileUpsertWithoutUserInput' - disconnect: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileWhereInput' - delete: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileWhereInput' - connect: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/ProfileUpdateToOneWithWhereWithoutUserInput' - - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' - Post_ItemUncheckedUpdateManyWithoutAuthorNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - connectOrCreate: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - upsert: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpsertWithWhereUniqueWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpsertWithWhereUniqueWithoutAuthorInput' - createMany: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInputEnvelope' - set: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - disconnect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - delete: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - connect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateWithWhereUniqueWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpdateWithWhereUniqueWithoutAuthorInput' - updateMany: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateManyWithWhereWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpdateManyWithWhereWithoutAuthorInput' - deleteMany: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - ProfileUncheckedUpdateOneWithoutUserNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - connectOrCreate: - $ref: '#/components/schemas/ProfileCreateOrConnectWithoutUserInput' - upsert: - $ref: '#/components/schemas/ProfileUpsertWithoutUserInput' - disconnect: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileWhereInput' - delete: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileWhereInput' - connect: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/ProfileUpdateToOneWithWhereWithoutUserInput' - - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' - UserCreateNestedOneWithoutProfileInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' - connectOrCreate: - $ref: '#/components/schemas/UserCreateOrConnectWithoutProfileInput' - connect: - $ref: '#/components/schemas/UserWhereUniqueInput' - NullableStringFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - nullable: true - UserUpdateOneRequiredWithoutProfileNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' - connectOrCreate: - $ref: '#/components/schemas/UserCreateOrConnectWithoutProfileInput' - upsert: - $ref: '#/components/schemas/UserUpsertWithoutProfileInput' - connect: - $ref: '#/components/schemas/UserWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/UserUpdateToOneWithWhereWithoutProfileInput' - - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' - UserCreateNestedOneWithoutPostsInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' - connectOrCreate: - $ref: '#/components/schemas/UserCreateOrConnectWithoutPostsInput' - connect: - $ref: '#/components/schemas/UserWhereUniqueInput' - BoolFieldUpdateOperationsInput: - type: object - properties: - set: - type: boolean - IntFieldUpdateOperationsInput: - type: object - properties: - set: - type: integer - increment: - type: integer - decrement: - type: integer - multiply: - type: integer - divide: - type: integer - UserUpdateOneWithoutPostsNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' - connectOrCreate: - $ref: '#/components/schemas/UserCreateOrConnectWithoutPostsInput' - upsert: - $ref: '#/components/schemas/UserUpsertWithoutPostsInput' - disconnect: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserWhereInput' - delete: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserWhereInput' - connect: - $ref: '#/components/schemas/UserWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/UserUpdateToOneWithWhereWithoutPostsInput' - - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' - NestedStringFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringFilter' - NestedDateTimeFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeFilter' - NestedEnumroleFilter: - type: object - properties: - equals: - $ref: '#/components/schemas/Role' - in: - type: array - items: - $ref: '#/components/schemas/Role' - notIn: - type: array - items: - $ref: '#/components/schemas/Role' - not: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/NestedEnumroleFilter' - NestedStringWithAggregatesFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedStringFilter' - _max: - $ref: '#/components/schemas/NestedStringFilter' - NestedIntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntFilter' - NestedDateTimeWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedDateTimeFilter' - _max: - $ref: '#/components/schemas/NestedDateTimeFilter' - NestedEnumroleWithAggregatesFilter: - type: object - properties: - equals: - $ref: '#/components/schemas/Role' - in: - type: array - items: - $ref: '#/components/schemas/Role' - notIn: - type: array - items: - $ref: '#/components/schemas/Role' - not: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/NestedEnumroleWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedEnumroleFilter' - _max: - $ref: '#/components/schemas/NestedEnumroleFilter' - NestedStringNullableFilter: - type: object - properties: - equals: - type: string - nullable: true - in: - type: array - items: - type: string - nullable: true - notIn: - type: array - items: - type: string - nullable: true - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringNullableFilter' - nullable: true - NestedStringNullableWithAggregatesFilter: - type: object - properties: - equals: - type: string - nullable: true - in: - type: array - items: - type: string - nullable: true - notIn: - type: array - items: - type: string - nullable: true - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringNullableWithAggregatesFilter' - nullable: true - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedStringNullableFilter' - _max: - $ref: '#/components/schemas/NestedStringNullableFilter' - NestedIntNullableFilter: - type: object - properties: - equals: - type: integer - nullable: true - in: - type: array - items: - type: integer - nullable: true - notIn: - type: array - items: - type: integer - nullable: true - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntNullableFilter' - nullable: true - NestedBoolFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolFilter' - NestedBoolWithAggregatesFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedBoolFilter' - _max: - $ref: '#/components/schemas/NestedBoolFilter' - NestedIntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedIntFilter' - _max: - $ref: '#/components/schemas/NestedIntFilter' - NestedFloatFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatFilter' - Post_ItemCreateWithoutAuthorInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - required: - - id - - title - Post_ItemUncheckedCreateWithoutAuthorInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - required: - - id - - title - Post_ItemCreateOrConnectWithoutAuthorInput: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - required: - - where - - create - Post_ItemCreateManyAuthorInputEnvelope: - type: object - properties: - data: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInput' - skipDuplicates: - type: boolean - required: - - data - ProfileCreateWithoutUserInput: - type: object - properties: - id: - type: string - image: - type: string - nullable: true - ProfileUncheckedCreateWithoutUserInput: - type: object - properties: - id: - type: string - image: - type: string - nullable: true - ProfileCreateOrConnectWithoutUserInput: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - required: - - where - - create - Post_ItemUpsertWithWhereUniqueWithoutAuthorInput: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedUpdateWithoutAuthorInput' - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - required: - - where - - update - - create - Post_ItemUpdateWithWhereUniqueWithoutAuthorInput: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - data: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedUpdateWithoutAuthorInput' - required: - - where - - data - Post_ItemUpdateManyWithWhereWithoutAuthorInput: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - data: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateManyMutationInput' - - $ref: '#/components/schemas/Post_ItemUncheckedUpdateManyWithoutAuthorInput' - required: - - where - - data - Post_ItemScalarWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - title: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - authorId: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - nullable: true - published: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - viewCount: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - notes: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - nullable: true - ProfileUpsertWithoutUserInput: - type: object - properties: - update: - oneOf: - - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - where: - $ref: '#/components/schemas/ProfileWhereInput' - required: - - update - - create - ProfileUpdateToOneWithWhereWithoutUserInput: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - data: - oneOf: - - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' - required: - - data - ProfileUpdateWithoutUserInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - image: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - ProfileUncheckedUpdateWithoutUserInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - image: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - UserCreateWithoutProfileInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - $ref: '#/components/schemas/Post_ItemCreateNestedManyWithoutAuthorInput' - required: - - email - UserUncheckedCreateWithoutProfileInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - $ref: "#/components/schemas/Post_ItemUncheckedCreateNestedManyWithoutAuthorInpu\ - t" - required: - - email - UserCreateOrConnectWithoutProfileInput: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' - required: - - where - - create - UserUpsertWithoutProfileInput: - type: object - properties: - update: - oneOf: - - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' - where: - $ref: '#/components/schemas/UserWhereInput' - required: - - update - - create - UserUpdateToOneWithWhereWithoutProfileInput: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - data: - oneOf: - - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' - required: - - data - UserUpdateWithoutProfileInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - posts: - $ref: '#/components/schemas/Post_ItemUpdateManyWithoutAuthorNestedInput' - UserUncheckedUpdateWithoutProfileInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - posts: - $ref: "#/components/schemas/Post_ItemUncheckedUpdateManyWithoutAuthorNestedInpu\ - t" - UserCreateWithoutPostsInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - profile: - $ref: '#/components/schemas/ProfileCreateNestedOneWithoutUserInput' - required: - - email - UserUncheckedCreateWithoutPostsInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - profile: - $ref: '#/components/schemas/ProfileUncheckedCreateNestedOneWithoutUserInput' - required: - - email - UserCreateOrConnectWithoutPostsInput: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' - required: - - where - - create - UserUpsertWithoutPostsInput: - type: object - properties: - update: - oneOf: - - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' - where: - $ref: '#/components/schemas/UserWhereInput' - required: - - update - - create - UserUpdateToOneWithWhereWithoutPostsInput: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - data: - oneOf: - - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' - required: - - data - UserUpdateWithoutPostsInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - profile: - $ref: '#/components/schemas/ProfileUpdateOneWithoutUserNestedInput' - UserUncheckedUpdateWithoutPostsInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - profile: - $ref: '#/components/schemas/ProfileUncheckedUpdateOneWithoutUserNestedInput' - Post_ItemCreateManyAuthorInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - required: - - id - - title - Post_ItemUpdateWithoutAuthorInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - Post_ItemUncheckedUpdateWithoutAuthorInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - Post_ItemUncheckedUpdateManyWithoutAuthorInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - nullable: true - UserDefaultArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - ProfileDefaultArgs: - type: object - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - UserInclude: - type: object - properties: - posts: - oneOf: - - type: boolean - - $ref: '#/components/schemas/Post_ItemFindManyArgs' - profile: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileDefaultArgs' - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserCountOutputTypeDefaultArgs' - ProfileInclude: - type: object - properties: - user: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserDefaultArgs' - Post_ItemInclude: - type: object - properties: - author: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserDefaultArgs' - UserCountOutputTypeSelect: - type: object - properties: - posts: - type: boolean - UserCountOutputTypeDefaultArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserCountOutputTypeSelect' - UserSelect: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - email: - type: boolean - role: - type: boolean - posts: - oneOf: - - type: boolean - - $ref: '#/components/schemas/Post_ItemFindManyArgs' - profile: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileDefaultArgs' - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserCountOutputTypeDefaultArgs' - ProfileSelect: - type: object - properties: - id: - type: boolean - image: - type: boolean - user: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserDefaultArgs' - userId: - type: boolean - Post_ItemSelect: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - title: - type: boolean - author: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserDefaultArgs' - authorId: - type: boolean - published: - type: boolean - viewCount: - type: boolean - notes: - type: boolean - UserCountAggregateInput: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - email: - type: boolean - role: - type: boolean - _all: - type: boolean - UserMinAggregateInput: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - email: - type: boolean - role: - type: boolean - UserMaxAggregateInput: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - email: - type: boolean - role: - type: boolean - ProfileCountAggregateInput: - type: object - properties: - id: - type: boolean - image: - type: boolean - userId: - type: boolean - _all: - type: boolean - ProfileMinAggregateInput: - type: object - properties: - id: - type: boolean - image: - type: boolean - userId: - type: boolean - ProfileMaxAggregateInput: - type: object - properties: - id: - type: boolean - image: - type: boolean - userId: - type: boolean - AggregateUser: - type: object - properties: - _count: - allOf: - - $ref: '#/components/schemas/UserCountAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/UserMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/UserMaxAggregateOutputType' - nullable: true - UserGroupByOutputType: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - _count: - allOf: - - $ref: '#/components/schemas/UserCountAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/UserMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/UserMaxAggregateOutputType' - nullable: true - required: - - id - - createdAt - - updatedAt - - email - - role - AggregateProfile: - type: object - properties: - _count: - allOf: - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/ProfileMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' - nullable: true - ProfileGroupByOutputType: - type: object - properties: - id: - type: string - image: - type: string - nullable: true - userId: - type: string - _count: - allOf: - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/ProfileMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' - nullable: true - required: - - id - - userId - AggregatePost_Item: - type: object - properties: - _count: - allOf: - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - nullable: true - _avg: - allOf: - - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' - nullable: true - _sum: - allOf: - - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' - nullable: true - Post_ItemGroupByOutputType: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - authorId: - type: string - nullable: true - published: - type: boolean - viewCount: - type: integer - notes: - type: string - nullable: true - _count: - allOf: - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - nullable: true - _avg: - allOf: - - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' - nullable: true - _sum: - allOf: - - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' - nullable: true - required: - - id - - createdAt - - updatedAt - - title - - published - - viewCount - UserCountAggregateOutputType: - type: object - properties: - id: - type: integer - createdAt: - type: integer - updatedAt: - type: integer - email: - type: integer - role: - type: integer - _all: - type: integer - required: - - id - - createdAt - - updatedAt - - email - - role - - _all - UserMinAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - createdAt: - type: string - format: date-time - nullable: true - updatedAt: - type: string - format: date-time - nullable: true - email: - type: string - nullable: true - role: - allOf: - - $ref: '#/components/schemas/Role' - nullable: true - UserMaxAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - createdAt: - type: string - format: date-time - nullable: true - updatedAt: - type: string - format: date-time - nullable: true - email: - type: string - nullable: true - role: - allOf: - - $ref: '#/components/schemas/Role' - nullable: true - ProfileCountAggregateOutputType: - type: object - properties: - id: - type: integer - image: - type: integer - userId: - type: integer - _all: - type: integer - required: - - id - - image - - userId - - _all - ProfileMinAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - image: - type: string - nullable: true - userId: - type: string - nullable: true - ProfileMaxAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - image: - type: string - nullable: true - userId: - type: string - nullable: true - Post_ItemCountAggregateOutputType: - type: object - properties: - id: - type: integer - createdAt: - type: integer - updatedAt: - type: integer - title: - type: integer - authorId: - type: integer - published: - type: integer - viewCount: - type: integer - notes: - type: integer - _all: - type: integer - required: - - id - - createdAt - - updatedAt - - title - - authorId - - published - - viewCount - - notes - - _all - Post_ItemAvgAggregateOutputType: - type: object - properties: - viewCount: - type: number - nullable: true - Post_ItemSumAggregateOutputType: - type: object - properties: - viewCount: - type: integer - nullable: true - Post_ItemMinAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - createdAt: - type: string - format: date-time - nullable: true - updatedAt: - type: string - format: date-time - nullable: true - title: - type: string - nullable: true - authorId: - type: string - nullable: true - published: - type: boolean - nullable: true - viewCount: - type: integer - nullable: true - notes: - type: string - nullable: true - Post_ItemMaxAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - createdAt: - type: string - format: date-time - nullable: true - updatedAt: - type: string - format: date-time - nullable: true - title: - type: string - nullable: true - authorId: - type: string - nullable: true - published: - type: boolean - nullable: true - viewCount: - type: integer - nullable: true - notes: - type: string - nullable: true - _Meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Serialization metadata - additionalProperties: true - _Error: - type: object - required: - - error - properties: - error: - type: object - required: - - message - properties: - prisma: - type: boolean - description: Indicates if the error occurred during a Prisma call - rejectedByPolicy: - type: boolean - description: Indicates if the error was due to rejection by a policy - code: - type: string - description: Prisma error code. Only available when "prisma" field is true. - message: - type: string - description: Error message - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - additionalProperties: true - BatchPayload: - type: object - properties: - count: - type: integer - UserCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - data: - $ref: '#/components/schemas/UserCreateInput' - meta: - $ref: '#/components/schemas/_Meta' - UserCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/UserCreateManyInput' - - type: array - items: - $ref: '#/components/schemas/UserCreateManyInput' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - UserFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - UserFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - UserFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/UserOrderByWithRelationInput' - - type: array - items: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/UserWhereUniqueInput' - take: - type: integer - skip: - type: integer - UserUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - data: - $ref: '#/components/schemas/UserUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - UserUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - data: - $ref: '#/components/schemas/UserUpdateManyMutationInput' - meta: - $ref: '#/components/schemas/_Meta' - UserUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - create: - $ref: '#/components/schemas/UserCreateInput' - update: - $ref: '#/components/schemas/UserUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - UserDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - UserDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - UserCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserSelect' - where: - $ref: '#/components/schemas/UserWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - UserAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - orderBy: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/UserWhereUniqueInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserCountAggregateInput' - _min: - $ref: '#/components/schemas/UserMinAggregateInput' - _max: - $ref: '#/components/schemas/UserMaxAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - UserGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - orderBy: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - by: - $ref: '#/components/schemas/UserScalarFieldEnum' - having: - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserCountAggregateInput' - _min: - $ref: '#/components/schemas/UserMinAggregateInput' - _max: - $ref: '#/components/schemas/UserMaxAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - data: - $ref: '#/components/schemas/ProfileCreateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/ProfileCreateManyInput' - - type: array - items: - $ref: '#/components/schemas/ProfileCreateManyInput' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - - type: array - items: - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - take: - type: integer - skip: - type: integer - ProfileUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - data: - $ref: '#/components/schemas/ProfileUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - data: - $ref: '#/components/schemas/ProfileUpdateManyMutationInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - create: - $ref: '#/components/schemas/ProfileCreateInput' - update: - $ref: '#/components/schemas/ProfileUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - where: - $ref: '#/components/schemas/ProfileWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - orderBy: - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileCountAggregateInput' - _min: - $ref: '#/components/schemas/ProfileMinAggregateInput' - _max: - $ref: '#/components/schemas/ProfileMaxAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - orderBy: - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - by: - $ref: '#/components/schemas/ProfileScalarFieldEnum' - having: - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileCountAggregateInput' - _min: - $ref: '#/components/schemas/ProfileMinAggregateInput' - _max: - $ref: '#/components/schemas/ProfileMaxAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - data: - $ref: '#/components/schemas/Post_ItemCreateInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateManyInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateManyInput' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/Post_ItemOrderByWithRelationInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - take: - type: integer - skip: - type: integer - Post_ItemUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - data: - $ref: '#/components/schemas/Post_ItemUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - data: - $ref: '#/components/schemas/Post_ItemUpdateManyMutationInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - create: - $ref: '#/components/schemas/Post_ItemCreateInput' - update: - $ref: '#/components/schemas/Post_ItemUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - orderBy: - $ref: '#/components/schemas/Post_ItemOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - orderBy: - $ref: '#/components/schemas/Post_ItemOrderByWithRelationInput' - by: - $ref: '#/components/schemas/Post_ItemScalarFieldEnum' - having: - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' -paths: - /user/create: - post: - operationId: createUser - description: Create a new User - tags: - - user - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreateArgs' - /user/createMany: - post: - operationId: createManyUser - description: Create several User - tags: - - user - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreateManyArgs' - /user/findUnique: - get: - operationId: findUniqueUser - description: Find one unique User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/findFirst: - get: - operationId: findFirstUser - description: Find the first User matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/findMany: - get: - operationId: findManyUser - description: Find users matching the given conditions - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/update: - patch: - operationId: updateUser - description: Update a User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdateArgs' - /user/updateMany: - patch: - operationId: updateManyUser - description: Update Users matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdateManyArgs' - /user/upsert: - post: - operationId: upsertUser - description: Upsert a User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpsertArgs' - /user/dodelete: - put: - operationId: deleteUser - description: Delete a unique user - tags: - - delete - - user - summary: Delete a user yeah yeah - deprecated: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserDeleteUniqueArgs' - /user/deleteMany: - delete: - operationId: deleteManyUser - description: Delete Users matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/count: - get: - operationId: countUser - description: Find a list of User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/UserCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/aggregate: - get: - operationId: aggregateUser - description: Aggregate Users - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateUser' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/groupBy: - get: - operationId: groupByUser - description: Group Users by fields - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/UserGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/create: - post: - operationId: createProfile - description: Create a new Profile - tags: - - profile - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCreateArgs' - /profile/createMany: - post: - operationId: createManyProfile - description: Create several Profile - tags: - - profile - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCreateManyArgs' - /profile/findUnique: - get: - operationId: findUniqueProfile - description: Find one unique Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/findFirst: - get: - operationId: findFirstProfile - description: Find the first Profile matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/findMany: - get: - operationId: findManyProfile - description: Find a list of Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/update: - patch: - operationId: updateProfile - description: Update a Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpdateArgs' - /profile/updateMany: - patch: - operationId: updateManyProfile - description: Update Profiles matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpdateManyArgs' - /profile/upsert: - post: - operationId: upsertProfile - description: Upsert a Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpsertArgs' - /profile/delete: - delete: - operationId: deleteProfile - description: Delete one unique Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/deleteMany: - delete: - operationId: deleteManyProfile - description: Delete Profiles matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/count: - get: - operationId: countProfile - description: Find a list of Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/aggregate: - get: - operationId: aggregateProfile - description: Aggregate Profiles - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateProfile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/groupBy: - get: - operationId: groupByProfile - description: Group Profiles by fields - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/ProfileGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/create: - post: - operationId: createPost_Item - description: Create a new Post_Item - tags: - - post_Item - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCreateArgs' - /post_Item/createMany: - post: - operationId: createManyPost_Item - description: Create several Post_Item - tags: - - post_Item - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCreateManyArgs' - /post_Item/findUnique: - get: - operationId: findUniquePost_Item - description: Find one unique Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/findFirst: - get: - operationId: findFirstPost_Item - description: Find the first Post_Item matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/update: - patch: - operationId: updatePost_Item - description: Update a Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpdateArgs' - /post_Item/updateMany: - patch: - operationId: updateManyPost_Item - description: Update Post_Items matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpdateManyArgs' - /post_Item/upsert: - post: - operationId: upsertPost_Item - description: Upsert a Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpsertArgs' - /post_Item/delete: - delete: - operationId: deletePost_Item - description: Delete one unique Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/deleteMany: - delete: - operationId: deleteManyPost_Item - description: Delete Post_Items matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/count: - get: - operationId: countPost_Item - description: Find a list of Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/aggregate: - get: - operationId: aggregatePost_Item - description: Aggregate Post_Items - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregatePost_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/groupBy: - get: - operationId: groupByPost_Item - description: Group Post_Items by fields - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Post_ItemGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.1.0-omit.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.1.0-omit.baseline.yaml deleted file mode 100644 index 11369e7d0..000000000 --- a/packages/plugins/openapi/tests/baseline/rpc-3.1.0-omit.baseline.yaml +++ /dev/null @@ -1,3058 +0,0 @@ -openapi: 3.1.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: user - description: User operations - - name: profile - description: Profile operations - - name: post_Item - description: Post-related operations -components: - schemas: - _AnyObject: - type: object - additionalProperties: true - Role: - type: string - enum: - - USER - - ADMIN - User: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - type: array - items: - $ref: '#/components/schemas/Post_Item' - profile: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Profile' - required: - - id - - createdAt - - updatedAt - - email - - role - Profile: - type: object - properties: - id: - type: string - image: - oneOf: - - type: 'null' - - type: string - user: - $ref: '#/components/schemas/User' - userId: - type: string - required: - - id - - user - - userId - Post_Item: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - author: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/User' - authorId: - oneOf: - - type: 'null' - - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - required: - - id - - createdAt - - updatedAt - - title - - published - - viewCount - AggregateUser: - type: object - properties: - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserCountAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserMaxAggregateOutputType' - UserGroupByOutputType: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserCountAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserMaxAggregateOutputType' - required: - - id - - createdAt - - updatedAt - - email - - role - AggregateProfile: - type: object - properties: - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' - ProfileGroupByOutputType: - type: object - properties: - id: - type: string - image: - oneOf: - - type: 'null' - - type: string - userId: - type: string - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' - required: - - id - - userId - AggregatePost_Item: - type: object - properties: - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - _avg: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' - _sum: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' - Post_ItemGroupByOutputType: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - authorId: - oneOf: - - type: 'null' - - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - _avg: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' - _sum: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' - required: - - id - - createdAt - - updatedAt - - title - - published - - viewCount - UserCountAggregateOutputType: - type: object - properties: - id: - type: integer - createdAt: - type: integer - updatedAt: - type: integer - email: - type: integer - role: - type: integer - _all: - type: integer - required: - - id - - createdAt - - updatedAt - - email - - role - - _all - UserMinAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - createdAt: - oneOf: - - type: 'null' - - type: string - format: date-time - updatedAt: - oneOf: - - type: 'null' - - type: string - format: date-time - email: - oneOf: - - type: 'null' - - type: string - role: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Role' - UserMaxAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - createdAt: - oneOf: - - type: 'null' - - type: string - format: date-time - updatedAt: - oneOf: - - type: 'null' - - type: string - format: date-time - email: - oneOf: - - type: 'null' - - type: string - role: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Role' - ProfileCountAggregateOutputType: - type: object - properties: - id: - type: integer - image: - type: integer - userId: - type: integer - _all: - type: integer - required: - - id - - image - - userId - - _all - ProfileMinAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - image: - oneOf: - - type: 'null' - - type: string - userId: - oneOf: - - type: 'null' - - type: string - ProfileMaxAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - image: - oneOf: - - type: 'null' - - type: string - userId: - oneOf: - - type: 'null' - - type: string - Post_ItemCountAggregateOutputType: - type: object - properties: - id: - type: integer - createdAt: - type: integer - updatedAt: - type: integer - title: - type: integer - authorId: - type: integer - published: - type: integer - viewCount: - type: integer - notes: - type: integer - _all: - type: integer - required: - - id - - createdAt - - updatedAt - - title - - authorId - - published - - viewCount - - notes - - _all - Post_ItemAvgAggregateOutputType: - type: object - properties: - viewCount: - oneOf: - - type: 'null' - - type: number - Post_ItemSumAggregateOutputType: - type: object - properties: - viewCount: - oneOf: - - type: 'null' - - type: integer - Post_ItemMinAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - createdAt: - oneOf: - - type: 'null' - - type: string - format: date-time - updatedAt: - oneOf: - - type: 'null' - - type: string - format: date-time - title: - oneOf: - - type: 'null' - - type: string - authorId: - oneOf: - - type: 'null' - - type: string - published: - oneOf: - - type: 'null' - - type: boolean - viewCount: - oneOf: - - type: 'null' - - type: integer - notes: - oneOf: - - type: 'null' - - type: string - Post_ItemMaxAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - createdAt: - oneOf: - - type: 'null' - - type: string - format: date-time - updatedAt: - oneOf: - - type: 'null' - - type: string - format: date-time - title: - oneOf: - - type: 'null' - - type: string - authorId: - oneOf: - - type: 'null' - - type: string - published: - oneOf: - - type: 'null' - - type: boolean - viewCount: - oneOf: - - type: 'null' - - type: integer - notes: - oneOf: - - type: 'null' - - type: string - _Meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Serialization metadata - additionalProperties: true - _Error: - type: object - required: - - error - properties: - error: - type: object - required: - - message - properties: - prisma: - type: boolean - description: Indicates if the error occurred during a Prisma call - rejectedByPolicy: - type: boolean - description: Indicates if the error was due to rejection by a policy - code: - type: string - description: Prisma error code. Only available when "prisma" field is true. - message: - type: string - description: Error message - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - additionalProperties: true - BatchPayload: - type: object - properties: - count: - type: integer - UserCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - UserFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - UserUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - create: - $ref: '#/components/schemas/_AnyObject' - update: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/_AnyObject' - _min: - $ref: '#/components/schemas/_AnyObject' - _max: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - UserGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - by: - $ref: '#/components/schemas/_AnyObject' - having: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/_AnyObject' - _min: - $ref: '#/components/schemas/_AnyObject' - _max: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - ProfileUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - create: - $ref: '#/components/schemas/_AnyObject' - update: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/_AnyObject' - _min: - $ref: '#/components/schemas/_AnyObject' - _max: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - ProfileGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - by: - $ref: '#/components/schemas/_AnyObject' - having: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/_AnyObject' - _min: - $ref: '#/components/schemas/_AnyObject' - _max: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/_AnyObject' - - type: array - items: - $ref: '#/components/schemas/_AnyObject' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/_AnyObject' - data: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - create: - $ref: '#/components/schemas/_AnyObject' - update: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/_AnyObject' - include: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/_AnyObject' - where: - $ref: '#/components/schemas/_AnyObject' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - cursor: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/_AnyObject' - orderBy: - $ref: '#/components/schemas/_AnyObject' - by: - $ref: '#/components/schemas/_AnyObject' - having: - $ref: '#/components/schemas/_AnyObject' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' -paths: - /user/create: - post: - operationId: createUser - description: Create a new User - tags: - - user - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreateArgs' - /user/createMany: - post: - operationId: createManyUser - description: Create several User - tags: - - user - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreateManyArgs' - /user/findUnique: - get: - operationId: findUniqueUser - description: Find one unique User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/findFirst: - get: - operationId: findFirstUser - description: Find the first User matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/findMany: - get: - operationId: findManyUser - description: Find users matching the given conditions - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/update: - patch: - operationId: updateUser - description: Update a User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdateArgs' - /user/updateMany: - patch: - operationId: updateManyUser - description: Update Users matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdateManyArgs' - /user/upsert: - post: - operationId: upsertUser - description: Upsert a User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpsertArgs' - /user/dodelete: - put: - operationId: deleteUser - description: Delete a unique user - tags: - - delete - - user - summary: Delete a user yeah yeah - deprecated: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserDeleteUniqueArgs' - /user/deleteMany: - delete: - operationId: deleteManyUser - description: Delete Users matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/count: - get: - operationId: countUser - description: Find a list of User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/UserCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/aggregate: - get: - operationId: aggregateUser - description: Aggregate Users - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateUser' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/groupBy: - get: - operationId: groupByUser - description: Group Users by fields - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/UserGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/create: - post: - operationId: createProfile - description: Create a new Profile - tags: - - profile - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCreateArgs' - /profile/createMany: - post: - operationId: createManyProfile - description: Create several Profile - tags: - - profile - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCreateManyArgs' - /profile/findUnique: - get: - operationId: findUniqueProfile - description: Find one unique Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/findFirst: - get: - operationId: findFirstProfile - description: Find the first Profile matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/findMany: - get: - operationId: findManyProfile - description: Find a list of Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/update: - patch: - operationId: updateProfile - description: Update a Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpdateArgs' - /profile/updateMany: - patch: - operationId: updateManyProfile - description: Update Profiles matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpdateManyArgs' - /profile/upsert: - post: - operationId: upsertProfile - description: Upsert a Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpsertArgs' - /profile/delete: - delete: - operationId: deleteProfile - description: Delete one unique Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/deleteMany: - delete: - operationId: deleteManyProfile - description: Delete Profiles matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/count: - get: - operationId: countProfile - description: Find a list of Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/aggregate: - get: - operationId: aggregateProfile - description: Aggregate Profiles - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateProfile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/groupBy: - get: - operationId: groupByProfile - description: Group Profiles by fields - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/ProfileGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/create: - post: - operationId: createPost_Item - description: Create a new Post_Item - tags: - - post_Item - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCreateArgs' - /post_Item/createMany: - post: - operationId: createManyPost_Item - description: Create several Post_Item - tags: - - post_Item - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCreateManyArgs' - /post_Item/findUnique: - get: - operationId: findUniquePost_Item - description: Find one unique Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/findFirst: - get: - operationId: findFirstPost_Item - description: Find the first Post_Item matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/update: - patch: - operationId: updatePost_Item - description: Update a Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpdateArgs' - /post_Item/updateMany: - patch: - operationId: updateManyPost_Item - description: Update Post_Items matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpdateManyArgs' - /post_Item/upsert: - post: - operationId: upsertPost_Item - description: Upsert a Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpsertArgs' - /post_Item/delete: - delete: - operationId: deletePost_Item - description: Delete one unique Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/deleteMany: - delete: - operationId: deleteManyPost_Item - description: Delete Post_Items matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/count: - get: - operationId: countPost_Item - description: Find a list of Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/aggregate: - get: - operationId: aggregatePost_Item - description: Aggregate Post_Items - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregatePost_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/groupBy: - get: - operationId: groupByPost_Item - description: Group Post_Items by fields - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Post_ItemGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} diff --git a/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml deleted file mode 100644 index 5c5103e09..000000000 --- a/packages/plugins/openapi/tests/baseline/rpc-3.1.0.baseline.yaml +++ /dev/null @@ -1,5853 +0,0 @@ -openapi: 3.1.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: user - description: User operations - - name: profile - description: Profile operations - - name: post_Item - description: Post-related operations -components: - schemas: - Role: - type: string - enum: - - USER - - ADMIN - UserScalarFieldEnum: - type: string - enum: - - id - - createdAt - - updatedAt - - email - - role - ProfileScalarFieldEnum: - type: string - enum: - - id - - image - - userId - Post_ItemScalarFieldEnum: - type: string - enum: - - id - - createdAt - - updatedAt - - title - - authorId - - published - - viewCount - - notes - SortOrder: - type: string - enum: - - asc - - desc - QueryMode: - type: string - enum: - - default - - insensitive - NullsOrder: - type: string - enum: - - first - - last - User: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - type: array - items: - $ref: '#/components/schemas/Post_Item' - profile: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Profile' - required: - - id - - createdAt - - updatedAt - - email - - role - Profile: - type: object - properties: - id: - type: string - image: - oneOf: - - type: 'null' - - type: string - user: - $ref: '#/components/schemas/User' - userId: - type: string - required: - - id - - user - - userId - Post_Item: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - author: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/User' - authorId: - oneOf: - - type: 'null' - - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - required: - - id - - createdAt - - updatedAt - - title - - published - - viewCount - UserWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/UserWhereInput' - - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/UserWhereInput' - - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - email: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - role: - oneOf: - - $ref: '#/components/schemas/EnumroleFilter' - - $ref: '#/components/schemas/Role' - posts: - $ref: '#/components/schemas/Post_ItemListRelationFilter' - profile: - oneOf: - - $ref: '#/components/schemas/ProfileNullableScalarRelationFilter' - - $ref: '#/components/schemas/ProfileWhereInput' - - type: 'null' - UserOrderByWithRelationInput: - type: object - properties: - id: - $ref: '#/components/schemas/SortOrder' - createdAt: - $ref: '#/components/schemas/SortOrder' - updatedAt: - $ref: '#/components/schemas/SortOrder' - email: - $ref: '#/components/schemas/SortOrder' - role: - $ref: '#/components/schemas/SortOrder' - posts: - $ref: '#/components/schemas/Post_ItemOrderByRelationAggregateInput' - profile: - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - UserWhereUniqueInput: - type: object - properties: - id: - type: string - email: - type: string - AND: - oneOf: - - $ref: '#/components/schemas/UserWhereInput' - - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/UserWhereInput' - - type: array - items: - $ref: '#/components/schemas/UserWhereInput' - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - role: - oneOf: - - $ref: '#/components/schemas/EnumroleFilter' - - $ref: '#/components/schemas/Role' - posts: - $ref: '#/components/schemas/Post_ItemListRelationFilter' - profile: - oneOf: - - $ref: '#/components/schemas/ProfileNullableScalarRelationFilter' - - $ref: '#/components/schemas/ProfileWhereInput' - - type: 'null' - UserScalarWhereWithAggregatesInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - OR: - type: array - items: - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - NOT: - oneOf: - - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - id: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - email: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - role: - oneOf: - - $ref: '#/components/schemas/EnumroleWithAggregatesFilter' - - $ref: '#/components/schemas/Role' - ProfileWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/ProfileWhereInput' - - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/ProfileWhereInput' - - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - image: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - - type: 'null' - userId: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - user: - oneOf: - - $ref: '#/components/schemas/UserScalarRelationFilter' - - $ref: '#/components/schemas/UserWhereInput' - ProfileOrderByWithRelationInput: - type: object - properties: - id: - $ref: '#/components/schemas/SortOrder' - image: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - userId: - $ref: '#/components/schemas/SortOrder' - user: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - ProfileWhereUniqueInput: - type: object - properties: - id: - type: string - userId: - type: string - AND: - oneOf: - - $ref: '#/components/schemas/ProfileWhereInput' - - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/ProfileWhereInput' - - type: array - items: - $ref: '#/components/schemas/ProfileWhereInput' - image: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - - type: 'null' - user: - oneOf: - - $ref: '#/components/schemas/UserScalarRelationFilter' - - $ref: '#/components/schemas/UserWhereInput' - ProfileScalarWhereWithAggregatesInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - OR: - type: array - items: - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - NOT: - oneOf: - - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - id: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - image: - oneOf: - - $ref: '#/components/schemas/StringNullableWithAggregatesFilter' - - type: string - - type: 'null' - userId: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - Post_ItemWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - title: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - authorId: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - - type: 'null' - published: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - viewCount: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - notes: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - - type: 'null' - author: - oneOf: - - $ref: '#/components/schemas/UserNullableScalarRelationFilter' - - $ref: '#/components/schemas/UserWhereInput' - - type: 'null' - Post_ItemOrderByWithRelationInput: - type: object - properties: - id: - $ref: '#/components/schemas/SortOrder' - createdAt: - $ref: '#/components/schemas/SortOrder' - updatedAt: - $ref: '#/components/schemas/SortOrder' - title: - $ref: '#/components/schemas/SortOrder' - authorId: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - published: - $ref: '#/components/schemas/SortOrder' - viewCount: - $ref: '#/components/schemas/SortOrder' - notes: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - author: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - Post_ItemWhereUniqueInput: - type: object - properties: - id: - type: string - AND: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereInput' - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - title: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - authorId: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - - type: 'null' - published: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - viewCount: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - notes: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - - type: 'null' - author: - oneOf: - - $ref: '#/components/schemas/UserNullableScalarRelationFilter' - - $ref: '#/components/schemas/UserWhereInput' - - type: 'null' - Post_ItemScalarWhereWithAggregatesInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - OR: - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - NOT: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - id: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - title: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - authorId: - oneOf: - - $ref: '#/components/schemas/StringNullableWithAggregatesFilter' - - type: string - - type: 'null' - published: - oneOf: - - $ref: '#/components/schemas/BoolWithAggregatesFilter' - - type: boolean - viewCount: - oneOf: - - $ref: '#/components/schemas/IntWithAggregatesFilter' - - type: integer - notes: - oneOf: - - $ref: '#/components/schemas/StringNullableWithAggregatesFilter' - - type: string - - type: 'null' - UserCreateInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - $ref: '#/components/schemas/Post_ItemCreateNestedManyWithoutAuthorInput' - profile: - $ref: '#/components/schemas/ProfileCreateNestedOneWithoutUserInput' - required: - - email - UserUpdateInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - posts: - $ref: '#/components/schemas/Post_ItemUpdateManyWithoutAuthorNestedInput' - profile: - $ref: '#/components/schemas/ProfileUpdateOneWithoutUserNestedInput' - UserCreateManyInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - required: - - email - UserUpdateManyMutationInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - ProfileCreateInput: - type: object - properties: - id: - type: string - image: - oneOf: - - type: 'null' - - type: string - user: - $ref: '#/components/schemas/UserCreateNestedOneWithoutProfileInput' - required: - - user - ProfileUpdateInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - image: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - user: - $ref: '#/components/schemas/UserUpdateOneRequiredWithoutProfileNestedInput' - ProfileCreateManyInput: - type: object - properties: - id: - type: string - image: - oneOf: - - type: 'null' - - type: string - userId: - type: string - required: - - userId - ProfileUpdateManyMutationInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - image: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - Post_ItemCreateInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - author: - $ref: '#/components/schemas/UserCreateNestedOneWithoutPostsInput' - required: - - id - - title - Post_ItemUpdateInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - author: - $ref: '#/components/schemas/UserUpdateOneWithoutPostsNestedInput' - Post_ItemCreateManyInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - authorId: - oneOf: - - type: 'null' - - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - required: - - id - - title - Post_ItemUpdateManyMutationInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - StringFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringFilter' - DateTimeFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeFilter' - EnumroleFilter: - type: object - properties: - equals: - $ref: '#/components/schemas/Role' - in: - type: array - items: - $ref: '#/components/schemas/Role' - notIn: - type: array - items: - $ref: '#/components/schemas/Role' - not: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/NestedEnumroleFilter' - Post_ItemListRelationFilter: - type: object - properties: - every: - $ref: '#/components/schemas/Post_ItemWhereInput' - some: - $ref: '#/components/schemas/Post_ItemWhereInput' - none: - $ref: '#/components/schemas/Post_ItemWhereInput' - ProfileNullableScalarRelationFilter: - type: object - properties: - is: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileWhereInput' - isNot: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileWhereInput' - Post_ItemOrderByRelationAggregateInput: - type: object - properties: - _count: - $ref: '#/components/schemas/SortOrder' - StringWithAggregatesFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedStringFilter' - _max: - $ref: '#/components/schemas/NestedStringFilter' - DateTimeWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedDateTimeFilter' - _max: - $ref: '#/components/schemas/NestedDateTimeFilter' - EnumroleWithAggregatesFilter: - type: object - properties: - equals: - $ref: '#/components/schemas/Role' - in: - type: array - items: - $ref: '#/components/schemas/Role' - notIn: - type: array - items: - $ref: '#/components/schemas/Role' - not: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/NestedEnumroleWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedEnumroleFilter' - _max: - $ref: '#/components/schemas/NestedEnumroleFilter' - StringNullableFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: string - in: - oneOf: - - type: 'null' - - type: array - items: - type: string - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringNullableFilter' - - type: 'null' - UserScalarRelationFilter: - type: object - properties: - is: - $ref: '#/components/schemas/UserWhereInput' - isNot: - $ref: '#/components/schemas/UserWhereInput' - SortOrderInput: - type: object - properties: - sort: - $ref: '#/components/schemas/SortOrder' - nulls: - $ref: '#/components/schemas/NullsOrder' - required: - - sort - StringNullableWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: string - in: - oneOf: - - type: 'null' - - type: array - items: - type: string - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringNullableWithAggregatesFilter' - - type: 'null' - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedStringNullableFilter' - _max: - $ref: '#/components/schemas/NestedStringNullableFilter' - BoolFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolFilter' - IntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntFilter' - UserNullableScalarRelationFilter: - type: object - properties: - is: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserWhereInput' - isNot: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserWhereInput' - BoolWithAggregatesFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedBoolFilter' - _max: - $ref: '#/components/schemas/NestedBoolFilter' - IntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedIntFilter' - _max: - $ref: '#/components/schemas/NestedIntFilter' - Post_ItemCreateNestedManyWithoutAuthorInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - connectOrCreate: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - createMany: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInputEnvelope' - connect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - ProfileCreateNestedOneWithoutUserInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - connectOrCreate: - $ref: '#/components/schemas/ProfileCreateOrConnectWithoutUserInput' - connect: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - Post_ItemUncheckedCreateNestedManyWithoutAuthorInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - connectOrCreate: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - createMany: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInputEnvelope' - connect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - ProfileUncheckedCreateNestedOneWithoutUserInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - connectOrCreate: - $ref: '#/components/schemas/ProfileCreateOrConnectWithoutUserInput' - connect: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - StringFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - DateTimeFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - format: date-time - EnumroleFieldUpdateOperationsInput: - type: object - properties: - set: - $ref: '#/components/schemas/Role' - Post_ItemUpdateManyWithoutAuthorNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - connectOrCreate: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - upsert: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpsertWithWhereUniqueWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpsertWithWhereUniqueWithoutAuthorInput' - createMany: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInputEnvelope' - set: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - disconnect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - delete: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - connect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateWithWhereUniqueWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpdateWithWhereUniqueWithoutAuthorInput' - updateMany: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateManyWithWhereWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpdateManyWithWhereWithoutAuthorInput' - deleteMany: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - ProfileUpdateOneWithoutUserNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - connectOrCreate: - $ref: '#/components/schemas/ProfileCreateOrConnectWithoutUserInput' - upsert: - $ref: '#/components/schemas/ProfileUpsertWithoutUserInput' - disconnect: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileWhereInput' - delete: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileWhereInput' - connect: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/ProfileUpdateToOneWithWhereWithoutUserInput' - - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' - Post_ItemUncheckedUpdateManyWithoutAuthorNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - connectOrCreate: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateOrConnectWithoutAuthorInput' - upsert: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpsertWithWhereUniqueWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpsertWithWhereUniqueWithoutAuthorInput' - createMany: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInputEnvelope' - set: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - disconnect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - delete: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - connect: - oneOf: - - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateWithWhereUniqueWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpdateWithWhereUniqueWithoutAuthorInput' - updateMany: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateManyWithWhereWithoutAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemUpdateManyWithWhereWithoutAuthorInput' - deleteMany: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - ProfileUncheckedUpdateOneWithoutUserNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - connectOrCreate: - $ref: '#/components/schemas/ProfileCreateOrConnectWithoutUserInput' - upsert: - $ref: '#/components/schemas/ProfileUpsertWithoutUserInput' - disconnect: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileWhereInput' - delete: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileWhereInput' - connect: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/ProfileUpdateToOneWithWhereWithoutUserInput' - - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' - UserCreateNestedOneWithoutProfileInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' - connectOrCreate: - $ref: '#/components/schemas/UserCreateOrConnectWithoutProfileInput' - connect: - $ref: '#/components/schemas/UserWhereUniqueInput' - NullableStringFieldUpdateOperationsInput: - type: object - properties: - set: - oneOf: - - type: 'null' - - type: string - UserUpdateOneRequiredWithoutProfileNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' - connectOrCreate: - $ref: '#/components/schemas/UserCreateOrConnectWithoutProfileInput' - upsert: - $ref: '#/components/schemas/UserUpsertWithoutProfileInput' - connect: - $ref: '#/components/schemas/UserWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/UserUpdateToOneWithWhereWithoutProfileInput' - - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' - UserCreateNestedOneWithoutPostsInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' - connectOrCreate: - $ref: '#/components/schemas/UserCreateOrConnectWithoutPostsInput' - connect: - $ref: '#/components/schemas/UserWhereUniqueInput' - BoolFieldUpdateOperationsInput: - type: object - properties: - set: - type: boolean - IntFieldUpdateOperationsInput: - type: object - properties: - set: - type: integer - increment: - type: integer - decrement: - type: integer - multiply: - type: integer - divide: - type: integer - UserUpdateOneWithoutPostsNestedInput: - type: object - properties: - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' - connectOrCreate: - $ref: '#/components/schemas/UserCreateOrConnectWithoutPostsInput' - upsert: - $ref: '#/components/schemas/UserUpsertWithoutPostsInput' - disconnect: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserWhereInput' - delete: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserWhereInput' - connect: - $ref: '#/components/schemas/UserWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/UserUpdateToOneWithWhereWithoutPostsInput' - - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' - NestedStringFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringFilter' - NestedDateTimeFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeFilter' - NestedEnumroleFilter: - type: object - properties: - equals: - $ref: '#/components/schemas/Role' - in: - type: array - items: - $ref: '#/components/schemas/Role' - notIn: - type: array - items: - $ref: '#/components/schemas/Role' - not: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/NestedEnumroleFilter' - NestedStringWithAggregatesFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedStringFilter' - _max: - $ref: '#/components/schemas/NestedStringFilter' - NestedIntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntFilter' - NestedDateTimeWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedDateTimeFilter' - _max: - $ref: '#/components/schemas/NestedDateTimeFilter' - NestedEnumroleWithAggregatesFilter: - type: object - properties: - equals: - $ref: '#/components/schemas/Role' - in: - type: array - items: - $ref: '#/components/schemas/Role' - notIn: - type: array - items: - $ref: '#/components/schemas/Role' - not: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/NestedEnumroleWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedEnumroleFilter' - _max: - $ref: '#/components/schemas/NestedEnumroleFilter' - NestedStringNullableFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: string - in: - oneOf: - - type: 'null' - - type: array - items: - type: string - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringNullableFilter' - - type: 'null' - NestedStringNullableWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: string - in: - oneOf: - - type: 'null' - - type: array - items: - type: string - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringNullableWithAggregatesFilter' - - type: 'null' - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedStringNullableFilter' - _max: - $ref: '#/components/schemas/NestedStringNullableFilter' - NestedIntNullableFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: integer - in: - oneOf: - - type: 'null' - - type: array - items: - type: integer - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntNullableFilter' - - type: 'null' - NestedBoolFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolFilter' - NestedBoolWithAggregatesFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedBoolFilter' - _max: - $ref: '#/components/schemas/NestedBoolFilter' - NestedIntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedIntFilter' - _max: - $ref: '#/components/schemas/NestedIntFilter' - NestedFloatFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatFilter' - Post_ItemCreateWithoutAuthorInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - required: - - id - - title - Post_ItemUncheckedCreateWithoutAuthorInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - required: - - id - - title - Post_ItemCreateOrConnectWithoutAuthorInput: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - required: - - where - - create - Post_ItemCreateManyAuthorInputEnvelope: - type: object - properties: - data: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateManyAuthorInput' - skipDuplicates: - type: boolean - required: - - data - ProfileCreateWithoutUserInput: - type: object - properties: - id: - type: string - image: - oneOf: - - type: 'null' - - type: string - ProfileUncheckedCreateWithoutUserInput: - type: object - properties: - id: - type: string - image: - oneOf: - - type: 'null' - - type: string - ProfileCreateOrConnectWithoutUserInput: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - required: - - where - - create - Post_ItemUpsertWithWhereUniqueWithoutAuthorInput: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - update: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedUpdateWithoutAuthorInput' - create: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedCreateWithoutAuthorInput' - required: - - where - - update - - create - Post_ItemUpdateWithWhereUniqueWithoutAuthorInput: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - data: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateWithoutAuthorInput' - - $ref: '#/components/schemas/Post_ItemUncheckedUpdateWithoutAuthorInput' - required: - - where - - data - Post_ItemUpdateManyWithWhereWithoutAuthorInput: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - data: - oneOf: - - $ref: '#/components/schemas/Post_ItemUpdateManyMutationInput' - - $ref: '#/components/schemas/Post_ItemUncheckedUpdateManyWithoutAuthorInput' - required: - - where - - data - Post_ItemScalarWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemScalarWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - createdAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - updatedAt: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - title: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - authorId: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - - type: 'null' - published: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - viewCount: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - notes: - oneOf: - - $ref: '#/components/schemas/StringNullableFilter' - - type: string - - type: 'null' - ProfileUpsertWithoutUserInput: - type: object - properties: - update: - oneOf: - - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' - create: - oneOf: - - $ref: '#/components/schemas/ProfileCreateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedCreateWithoutUserInput' - where: - $ref: '#/components/schemas/ProfileWhereInput' - required: - - update - - create - ProfileUpdateToOneWithWhereWithoutUserInput: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - data: - oneOf: - - $ref: '#/components/schemas/ProfileUpdateWithoutUserInput' - - $ref: '#/components/schemas/ProfileUncheckedUpdateWithoutUserInput' - required: - - data - ProfileUpdateWithoutUserInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - image: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - ProfileUncheckedUpdateWithoutUserInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - image: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - UserCreateWithoutProfileInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - $ref: '#/components/schemas/Post_ItemCreateNestedManyWithoutAuthorInput' - required: - - email - UserUncheckedCreateWithoutProfileInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - posts: - $ref: "#/components/schemas/Post_ItemUncheckedCreateNestedManyWithoutAuthorInpu\ - t" - required: - - email - UserCreateOrConnectWithoutProfileInput: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' - required: - - where - - create - UserUpsertWithoutProfileInput: - type: object - properties: - update: - oneOf: - - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutProfileInput' - where: - $ref: '#/components/schemas/UserWhereInput' - required: - - update - - create - UserUpdateToOneWithWhereWithoutProfileInput: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - data: - oneOf: - - $ref: '#/components/schemas/UserUpdateWithoutProfileInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutProfileInput' - required: - - data - UserUpdateWithoutProfileInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - posts: - $ref: '#/components/schemas/Post_ItemUpdateManyWithoutAuthorNestedInput' - UserUncheckedUpdateWithoutProfileInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - posts: - $ref: "#/components/schemas/Post_ItemUncheckedUpdateManyWithoutAuthorNestedInpu\ - t" - UserCreateWithoutPostsInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - profile: - $ref: '#/components/schemas/ProfileCreateNestedOneWithoutUserInput' - required: - - email - UserUncheckedCreateWithoutPostsInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - profile: - $ref: '#/components/schemas/ProfileUncheckedCreateNestedOneWithoutUserInput' - required: - - email - UserCreateOrConnectWithoutPostsInput: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' - required: - - where - - create - UserUpsertWithoutPostsInput: - type: object - properties: - update: - oneOf: - - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' - create: - oneOf: - - $ref: '#/components/schemas/UserCreateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedCreateWithoutPostsInput' - where: - $ref: '#/components/schemas/UserWhereInput' - required: - - update - - create - UserUpdateToOneWithWhereWithoutPostsInput: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - data: - oneOf: - - $ref: '#/components/schemas/UserUpdateWithoutPostsInput' - - $ref: '#/components/schemas/UserUncheckedUpdateWithoutPostsInput' - required: - - data - UserUpdateWithoutPostsInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - profile: - $ref: '#/components/schemas/ProfileUpdateOneWithoutUserNestedInput' - UserUncheckedUpdateWithoutPostsInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - email: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - role: - oneOf: - - $ref: '#/components/schemas/Role' - - $ref: '#/components/schemas/EnumroleFieldUpdateOperationsInput' - profile: - $ref: '#/components/schemas/ProfileUncheckedUpdateOneWithoutUserNestedInput' - Post_ItemCreateManyAuthorInput: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - required: - - id - - title - Post_ItemUpdateWithoutAuthorInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - Post_ItemUncheckedUpdateWithoutAuthorInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - Post_ItemUncheckedUpdateManyWithoutAuthorInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - createdAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - updatedAt: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - title: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - published: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - viewCount: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - notes: - oneOf: - - type: string - - $ref: '#/components/schemas/NullableStringFieldUpdateOperationsInput' - - type: 'null' - UserDefaultArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - ProfileDefaultArgs: - type: object - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - UserInclude: - type: object - properties: - posts: - oneOf: - - type: boolean - - $ref: '#/components/schemas/Post_ItemFindManyArgs' - profile: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileDefaultArgs' - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserCountOutputTypeDefaultArgs' - ProfileInclude: - type: object - properties: - user: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserDefaultArgs' - Post_ItemInclude: - type: object - properties: - author: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserDefaultArgs' - UserCountOutputTypeSelect: - type: object - properties: - posts: - type: boolean - UserCountOutputTypeDefaultArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserCountOutputTypeSelect' - UserSelect: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - email: - type: boolean - role: - type: boolean - posts: - oneOf: - - type: boolean - - $ref: '#/components/schemas/Post_ItemFindManyArgs' - profile: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileDefaultArgs' - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserCountOutputTypeDefaultArgs' - ProfileSelect: - type: object - properties: - id: - type: boolean - image: - type: boolean - user: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserDefaultArgs' - userId: - type: boolean - Post_ItemSelect: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - title: - type: boolean - author: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserDefaultArgs' - authorId: - type: boolean - published: - type: boolean - viewCount: - type: boolean - notes: - type: boolean - UserCountAggregateInput: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - email: - type: boolean - role: - type: boolean - _all: - type: boolean - UserMinAggregateInput: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - email: - type: boolean - role: - type: boolean - UserMaxAggregateInput: - type: object - properties: - id: - type: boolean - createdAt: - type: boolean - updatedAt: - type: boolean - email: - type: boolean - role: - type: boolean - ProfileCountAggregateInput: - type: object - properties: - id: - type: boolean - image: - type: boolean - userId: - type: boolean - _all: - type: boolean - ProfileMinAggregateInput: - type: object - properties: - id: - type: boolean - image: - type: boolean - userId: - type: boolean - ProfileMaxAggregateInput: - type: object - properties: - id: - type: boolean - image: - type: boolean - userId: - type: boolean - AggregateUser: - type: object - properties: - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserCountAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserMaxAggregateOutputType' - UserGroupByOutputType: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - email: - type: string - role: - $ref: '#/components/schemas/Role' - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserCountAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/UserMaxAggregateOutputType' - required: - - id - - createdAt - - updatedAt - - email - - role - AggregateProfile: - type: object - properties: - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' - ProfileGroupByOutputType: - type: object - properties: - id: - type: string - image: - oneOf: - - type: 'null' - - type: string - userId: - type: string - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/ProfileMaxAggregateOutputType' - required: - - id - - userId - AggregatePost_Item: - type: object - properties: - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - _avg: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' - _sum: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' - Post_ItemGroupByOutputType: - type: object - properties: - id: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - title: - type: string - authorId: - oneOf: - - type: 'null' - - type: string - published: - type: boolean - viewCount: - type: integer - notes: - oneOf: - - type: 'null' - - type: string - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - _avg: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemAvgAggregateOutputType' - _sum: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemSumAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Post_ItemMaxAggregateOutputType' - required: - - id - - createdAt - - updatedAt - - title - - published - - viewCount - UserCountAggregateOutputType: - type: object - properties: - id: - type: integer - createdAt: - type: integer - updatedAt: - type: integer - email: - type: integer - role: - type: integer - _all: - type: integer - required: - - id - - createdAt - - updatedAt - - email - - role - - _all - UserMinAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - createdAt: - oneOf: - - type: 'null' - - type: string - format: date-time - updatedAt: - oneOf: - - type: 'null' - - type: string - format: date-time - email: - oneOf: - - type: 'null' - - type: string - role: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Role' - UserMaxAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - createdAt: - oneOf: - - type: 'null' - - type: string - format: date-time - updatedAt: - oneOf: - - type: 'null' - - type: string - format: date-time - email: - oneOf: - - type: 'null' - - type: string - role: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Role' - ProfileCountAggregateOutputType: - type: object - properties: - id: - type: integer - image: - type: integer - userId: - type: integer - _all: - type: integer - required: - - id - - image - - userId - - _all - ProfileMinAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - image: - oneOf: - - type: 'null' - - type: string - userId: - oneOf: - - type: 'null' - - type: string - ProfileMaxAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - image: - oneOf: - - type: 'null' - - type: string - userId: - oneOf: - - type: 'null' - - type: string - Post_ItemCountAggregateOutputType: - type: object - properties: - id: - type: integer - createdAt: - type: integer - updatedAt: - type: integer - title: - type: integer - authorId: - type: integer - published: - type: integer - viewCount: - type: integer - notes: - type: integer - _all: - type: integer - required: - - id - - createdAt - - updatedAt - - title - - authorId - - published - - viewCount - - notes - - _all - Post_ItemAvgAggregateOutputType: - type: object - properties: - viewCount: - oneOf: - - type: 'null' - - type: number - Post_ItemSumAggregateOutputType: - type: object - properties: - viewCount: - oneOf: - - type: 'null' - - type: integer - Post_ItemMinAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - createdAt: - oneOf: - - type: 'null' - - type: string - format: date-time - updatedAt: - oneOf: - - type: 'null' - - type: string - format: date-time - title: - oneOf: - - type: 'null' - - type: string - authorId: - oneOf: - - type: 'null' - - type: string - published: - oneOf: - - type: 'null' - - type: boolean - viewCount: - oneOf: - - type: 'null' - - type: integer - notes: - oneOf: - - type: 'null' - - type: string - Post_ItemMaxAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - createdAt: - oneOf: - - type: 'null' - - type: string - format: date-time - updatedAt: - oneOf: - - type: 'null' - - type: string - format: date-time - title: - oneOf: - - type: 'null' - - type: string - authorId: - oneOf: - - type: 'null' - - type: string - published: - oneOf: - - type: 'null' - - type: boolean - viewCount: - oneOf: - - type: 'null' - - type: integer - notes: - oneOf: - - type: 'null' - - type: string - _Meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Serialization metadata - additionalProperties: true - _Error: - type: object - required: - - error - properties: - error: - type: object - required: - - message - properties: - prisma: - type: boolean - description: Indicates if the error occurred during a Prisma call - rejectedByPolicy: - type: boolean - description: Indicates if the error was due to rejection by a policy - code: - type: string - description: Prisma error code. Only available when "prisma" field is true. - message: - type: string - description: Error message - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - additionalProperties: true - BatchPayload: - type: object - properties: - count: - type: integer - UserCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - data: - $ref: '#/components/schemas/UserCreateInput' - meta: - $ref: '#/components/schemas/_Meta' - UserCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/UserCreateManyInput' - - type: array - items: - $ref: '#/components/schemas/UserCreateManyInput' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - UserFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - UserFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - UserFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/UserOrderByWithRelationInput' - - type: array - items: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/UserWhereUniqueInput' - take: - type: integer - skip: - type: integer - UserUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - data: - $ref: '#/components/schemas/UserUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - UserUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - data: - $ref: '#/components/schemas/UserUpdateManyMutationInput' - meta: - $ref: '#/components/schemas/_Meta' - UserUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - create: - $ref: '#/components/schemas/UserCreateInput' - update: - $ref: '#/components/schemas/UserUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - UserDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/UserSelect' - include: - $ref: '#/components/schemas/UserInclude' - where: - $ref: '#/components/schemas/UserWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - UserDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - UserCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/UserSelect' - where: - $ref: '#/components/schemas/UserWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - UserAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - orderBy: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/UserWhereUniqueInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserCountAggregateInput' - _min: - $ref: '#/components/schemas/UserMinAggregateInput' - _max: - $ref: '#/components/schemas/UserMaxAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - UserGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/UserWhereInput' - orderBy: - $ref: '#/components/schemas/UserOrderByWithRelationInput' - by: - $ref: '#/components/schemas/UserScalarFieldEnum' - having: - $ref: '#/components/schemas/UserScalarWhereWithAggregatesInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/UserCountAggregateInput' - _min: - $ref: '#/components/schemas/UserMinAggregateInput' - _max: - $ref: '#/components/schemas/UserMaxAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - data: - $ref: '#/components/schemas/ProfileCreateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/ProfileCreateManyInput' - - type: array - items: - $ref: '#/components/schemas/ProfileCreateManyInput' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - - type: array - items: - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - take: - type: integer - skip: - type: integer - ProfileUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - data: - $ref: '#/components/schemas/ProfileUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - data: - $ref: '#/components/schemas/ProfileUpdateManyMutationInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - create: - $ref: '#/components/schemas/ProfileCreateInput' - update: - $ref: '#/components/schemas/ProfileUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - include: - $ref: '#/components/schemas/ProfileInclude' - where: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/ProfileSelect' - where: - $ref: '#/components/schemas/ProfileWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - orderBy: - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/ProfileWhereUniqueInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileCountAggregateInput' - _min: - $ref: '#/components/schemas/ProfileMinAggregateInput' - _max: - $ref: '#/components/schemas/ProfileMaxAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - ProfileGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/ProfileWhereInput' - orderBy: - $ref: '#/components/schemas/ProfileOrderByWithRelationInput' - by: - $ref: '#/components/schemas/ProfileScalarFieldEnum' - having: - $ref: '#/components/schemas/ProfileScalarWhereWithAggregatesInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/ProfileCountAggregateInput' - _min: - $ref: '#/components/schemas/ProfileMinAggregateInput' - _max: - $ref: '#/components/schemas/ProfileMaxAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - data: - $ref: '#/components/schemas/Post_ItemCreateInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/Post_ItemCreateManyInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemCreateManyInput' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - orderBy: - oneOf: - - $ref: '#/components/schemas/Post_ItemOrderByWithRelationInput' - - type: array - items: - $ref: '#/components/schemas/Post_ItemOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - take: - type: integer - skip: - type: integer - Post_ItemUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - data: - $ref: '#/components/schemas/Post_ItemUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - data: - $ref: '#/components/schemas/Post_ItemUpdateManyMutationInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - create: - $ref: '#/components/schemas/Post_ItemCreateInput' - update: - $ref: '#/components/schemas/Post_ItemUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - include: - $ref: '#/components/schemas/Post_ItemInclude' - where: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/Post_ItemSelect' - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - orderBy: - $ref: '#/components/schemas/Post_ItemOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/Post_ItemWhereUniqueInput' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' - Post_ItemGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/Post_ItemWhereInput' - orderBy: - $ref: '#/components/schemas/Post_ItemOrderByWithRelationInput' - by: - $ref: '#/components/schemas/Post_ItemScalarFieldEnum' - having: - $ref: '#/components/schemas/Post_ItemScalarWhereWithAggregatesInput' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' -paths: - /user/create: - post: - operationId: createUser - description: Create a new User - tags: - - user - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreateArgs' - /user/createMany: - post: - operationId: createManyUser - description: Create several User - tags: - - user - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserCreateManyArgs' - /user/findUnique: - get: - operationId: findUniqueUser - description: Find one unique User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/findFirst: - get: - operationId: findFirstUser - description: Find the first User matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/findMany: - get: - operationId: findManyUser - description: Find users matching the given conditions - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/update: - patch: - operationId: updateUser - description: Update a User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdateArgs' - /user/updateMany: - patch: - operationId: updateManyUser - description: Update Users matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpdateManyArgs' - /user/upsert: - post: - operationId: upsertUser - description: Upsert a User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserUpsertArgs' - /user/dodelete: - put: - operationId: deleteUser - description: Delete a unique user - tags: - - delete - - user - summary: Delete a user yeah yeah - deprecated: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/User' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserDeleteUniqueArgs' - /user/deleteMany: - delete: - operationId: deleteManyUser - description: Delete Users matching the given condition - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/count: - get: - operationId: countUser - description: Find a list of User - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/UserCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/aggregate: - get: - operationId: aggregateUser - description: Aggregate Users - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateUser' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /user/groupBy: - get: - operationId: groupByUser - description: Group Users by fields - tags: - - user - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/UserGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/UserGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/create: - post: - operationId: createProfile - description: Create a new Profile - tags: - - profile - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCreateArgs' - /profile/createMany: - post: - operationId: createManyProfile - description: Create several Profile - tags: - - profile - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCreateManyArgs' - /profile/findUnique: - get: - operationId: findUniqueProfile - description: Find one unique Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/findFirst: - get: - operationId: findFirstProfile - description: Find the first Profile matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/findMany: - get: - operationId: findManyProfile - description: Find a list of Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/update: - patch: - operationId: updateProfile - description: Update a Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpdateArgs' - /profile/updateMany: - patch: - operationId: updateManyProfile - description: Update Profiles matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpdateManyArgs' - /profile/upsert: - post: - operationId: upsertProfile - description: Upsert a Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileUpsertArgs' - /profile/delete: - delete: - operationId: deleteProfile - description: Delete one unique Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Profile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/deleteMany: - delete: - operationId: deleteManyProfile - description: Delete Profiles matching the given condition - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/count: - get: - operationId: countProfile - description: Find a list of Profile - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/ProfileCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/aggregate: - get: - operationId: aggregateProfile - description: Aggregate Profiles - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateProfile' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /profile/groupBy: - get: - operationId: groupByProfile - description: Group Profiles by fields - tags: - - profile - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/ProfileGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/ProfileGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/create: - post: - operationId: createPost_Item - description: Create a new Post_Item - tags: - - post_Item - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCreateArgs' - /post_Item/createMany: - post: - operationId: createManyPost_Item - description: Create several Post_Item - tags: - - post_Item - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCreateManyArgs' - /post_Item/findUnique: - get: - operationId: findUniquePost_Item - description: Find one unique Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/findFirst: - get: - operationId: findFirstPost_Item - description: Find the first Post_Item matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/update: - patch: - operationId: updatePost_Item - description: Update a Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpdateArgs' - /post_Item/updateMany: - patch: - operationId: updateManyPost_Item - description: Update Post_Items matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpdateManyArgs' - /post_Item/upsert: - post: - operationId: upsertPost_Item - description: Upsert a Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemUpsertArgs' - /post_Item/delete: - delete: - operationId: deletePost_Item - description: Delete one unique Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Post_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/deleteMany: - delete: - operationId: deleteManyPost_Item - description: Delete Post_Items matching the given condition - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/count: - get: - operationId: countPost_Item - description: Find a list of Post_Item - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/Post_ItemCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/aggregate: - get: - operationId: aggregatePost_Item - description: Aggregate Post_Items - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregatePost_Item' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /post_Item/groupBy: - get: - operationId: groupByPost_Item - description: Group Post_Items by fields - tags: - - post_Item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Post_ItemGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/Post_ItemGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} diff --git a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml deleted file mode 100644 index eaea71827..000000000 --- a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.0.0.baseline.yaml +++ /dev/null @@ -1,3094 +0,0 @@ -openapi: 3.0.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: foo - description: Foo operations -components: - schemas: - FooScalarFieldEnum: - type: string - enum: - - id - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - - json - - plainJson - SortOrder: - type: string - enum: - - asc - - desc - NullableJsonNullValueInput: - type: string - enum: - - DbNull - - JsonNull - JsonNullValueInput: - type: string - enum: - - JsonNull - QueryMode: - type: string - enum: - - default - - insensitive - JsonNullValueFilter: - type: string - enum: - - DbNull - - JsonNull - - AnyNull - NullsOrder: - type: string - enum: - - first - - last - Foo: - type: object - properties: - id: - type: string - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: string - - type: number - boolean: - type: boolean - bytes: - type: string - format: byte - nullable: true - json: - allOf: - - $ref: '#/components/schemas/Meta' - nullable: true - plainJson: {} - required: - - id - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - plainJson - Meta: - type: object - description: The "Meta" TypeDef - properties: - something: - type: string - required: - - something - FooWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/FooWhereInput' - - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/FooWhereInput' - - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - string: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - int: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - bigInt: - oneOf: - - $ref: '#/components/schemas/BigIntFilter' - - type: integer - date: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - float: - oneOf: - - $ref: '#/components/schemas/FloatFilter' - - type: number - decimal: - oneOf: - - $ref: '#/components/schemas/DecimalFilter' - - oneOf: - - type: string - - type: number - boolean: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - bytes: - oneOf: - - $ref: '#/components/schemas/BytesNullableFilter' - - type: string - format: byte - nullable: true - json: - $ref: '#/components/schemas/JsonNullableFilter' - plainJson: - $ref: '#/components/schemas/JsonFilter' - FooOrderByWithRelationInput: - type: object - properties: - id: - $ref: '#/components/schemas/SortOrder' - string: - $ref: '#/components/schemas/SortOrder' - int: - $ref: '#/components/schemas/SortOrder' - bigInt: - $ref: '#/components/schemas/SortOrder' - date: - $ref: '#/components/schemas/SortOrder' - float: - $ref: '#/components/schemas/SortOrder' - decimal: - $ref: '#/components/schemas/SortOrder' - boolean: - $ref: '#/components/schemas/SortOrder' - bytes: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - json: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - plainJson: - $ref: '#/components/schemas/SortOrder' - FooWhereUniqueInput: - type: object - properties: - id: - type: string - AND: - oneOf: - - $ref: '#/components/schemas/FooWhereInput' - - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/FooWhereInput' - - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - string: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - int: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - bigInt: - oneOf: - - $ref: '#/components/schemas/BigIntFilter' - - type: integer - date: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - float: - oneOf: - - $ref: '#/components/schemas/FloatFilter' - - type: number - decimal: - oneOf: - - $ref: '#/components/schemas/DecimalFilter' - - oneOf: - - type: string - - type: number - boolean: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - bytes: - oneOf: - - $ref: '#/components/schemas/BytesNullableFilter' - - type: string - format: byte - nullable: true - json: - $ref: '#/components/schemas/JsonNullableFilter' - plainJson: - $ref: '#/components/schemas/JsonFilter' - FooScalarWhereWithAggregatesInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - OR: - type: array - items: - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - NOT: - oneOf: - - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - id: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - string: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - int: - oneOf: - - $ref: '#/components/schemas/IntWithAggregatesFilter' - - type: integer - bigInt: - oneOf: - - $ref: '#/components/schemas/BigIntWithAggregatesFilter' - - type: integer - date: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - float: - oneOf: - - $ref: '#/components/schemas/FloatWithAggregatesFilter' - - type: number - decimal: - oneOf: - - $ref: '#/components/schemas/DecimalWithAggregatesFilter' - - oneOf: - - type: string - - type: number - boolean: - oneOf: - - $ref: '#/components/schemas/BoolWithAggregatesFilter' - - type: boolean - bytes: - oneOf: - - $ref: '#/components/schemas/BytesNullableWithAggregatesFilter' - - type: string - format: byte - nullable: true - json: - $ref: '#/components/schemas/JsonNullableWithAggregatesFilter' - plainJson: - $ref: '#/components/schemas/JsonWithAggregatesFilter' - FooCreateInput: - type: object - properties: - id: - type: string - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: string - - type: number - boolean: - type: boolean - bytes: - type: string - format: byte - nullable: true - json: - oneOf: - - $ref: '#/components/schemas/NullableJsonNullValueInput' - - {} - plainJson: - oneOf: - - $ref: '#/components/schemas/JsonNullValueInput' - - {} - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - plainJson - FooUpdateInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - string: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - int: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - bigInt: - oneOf: - - type: integer - - $ref: '#/components/schemas/BigIntFieldUpdateOperationsInput' - date: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - float: - oneOf: - - type: number - - $ref: '#/components/schemas/FloatFieldUpdateOperationsInput' - decimal: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/DecimalFieldUpdateOperationsInput' - boolean: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - bytes: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NullableBytesFieldUpdateOperationsInput' - nullable: true - json: - oneOf: - - $ref: '#/components/schemas/NullableJsonNullValueInput' - - {} - plainJson: - oneOf: - - $ref: '#/components/schemas/JsonNullValueInput' - - {} - FooCreateManyInput: - type: object - properties: - id: - type: string - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: string - - type: number - boolean: - type: boolean - bytes: - type: string - format: byte - nullable: true - json: - oneOf: - - $ref: '#/components/schemas/NullableJsonNullValueInput' - - {} - plainJson: - oneOf: - - $ref: '#/components/schemas/JsonNullValueInput' - - {} - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - plainJson - FooUpdateManyMutationInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - string: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - int: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - bigInt: - oneOf: - - type: integer - - $ref: '#/components/schemas/BigIntFieldUpdateOperationsInput' - date: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - float: - oneOf: - - type: number - - $ref: '#/components/schemas/FloatFieldUpdateOperationsInput' - decimal: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/DecimalFieldUpdateOperationsInput' - boolean: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - bytes: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NullableBytesFieldUpdateOperationsInput' - nullable: true - json: - oneOf: - - $ref: '#/components/schemas/NullableJsonNullValueInput' - - {} - plainJson: - oneOf: - - $ref: '#/components/schemas/JsonNullValueInput' - - {} - StringFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringFilter' - IntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntFilter' - BigIntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedBigIntFilter' - DateTimeFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeFilter' - FloatFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatFilter' - DecimalFilter: - type: object - properties: - equals: - oneOf: - - type: string - - type: number - in: - type: array - items: - oneOf: - - type: string - - type: number - notIn: - type: array - items: - oneOf: - - type: string - - type: number - lt: - oneOf: - - type: string - - type: number - lte: - oneOf: - - type: string - - type: number - gt: - oneOf: - - type: string - - type: number - gte: - oneOf: - - type: string - - type: number - not: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/NestedDecimalFilter' - BoolFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolFilter' - BytesNullableFilter: - type: object - properties: - equals: - type: string - format: byte - nullable: true - in: - type: array - items: - type: string - format: byte - nullable: true - notIn: - type: array - items: - type: string - format: byte - nullable: true - not: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NestedBytesNullableFilter' - nullable: true - JsonNullableFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - nullable: true - array_ends_with: - nullable: true - array_contains: - nullable: true - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - JsonFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - nullable: true - array_ends_with: - nullable: true - array_contains: - nullable: true - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - SortOrderInput: - type: object - properties: - sort: - $ref: '#/components/schemas/SortOrder' - nulls: - $ref: '#/components/schemas/NullsOrder' - required: - - sort - StringWithAggregatesFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedStringFilter' - _max: - $ref: '#/components/schemas/NestedStringFilter' - IntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedIntFilter' - _max: - $ref: '#/components/schemas/NestedIntFilter' - BigIntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedBigIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedBigIntFilter' - _min: - $ref: '#/components/schemas/NestedBigIntFilter' - _max: - $ref: '#/components/schemas/NestedBigIntFilter' - DateTimeWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedDateTimeFilter' - _max: - $ref: '#/components/schemas/NestedDateTimeFilter' - FloatWithAggregatesFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedFloatFilter' - _min: - $ref: '#/components/schemas/NestedFloatFilter' - _max: - $ref: '#/components/schemas/NestedFloatFilter' - DecimalWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - type: string - - type: number - in: - type: array - items: - oneOf: - - type: string - - type: number - notIn: - type: array - items: - oneOf: - - type: string - - type: number - lt: - oneOf: - - type: string - - type: number - lte: - oneOf: - - type: string - - type: number - gt: - oneOf: - - type: string - - type: number - gte: - oneOf: - - type: string - - type: number - not: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/NestedDecimalWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedDecimalFilter' - _sum: - $ref: '#/components/schemas/NestedDecimalFilter' - _min: - $ref: '#/components/schemas/NestedDecimalFilter' - _max: - $ref: '#/components/schemas/NestedDecimalFilter' - BoolWithAggregatesFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedBoolFilter' - _max: - $ref: '#/components/schemas/NestedBoolFilter' - BytesNullableWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: byte - nullable: true - in: - type: array - items: - type: string - format: byte - nullable: true - notIn: - type: array - items: - type: string - format: byte - nullable: true - not: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NestedBytesNullableWithAggregatesFilter' - nullable: true - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedBytesNullableFilter' - _max: - $ref: '#/components/schemas/NestedBytesNullableFilter' - JsonNullableWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - nullable: true - array_ends_with: - nullable: true - array_contains: - nullable: true - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedJsonNullableFilter' - _max: - $ref: '#/components/schemas/NestedJsonNullableFilter' - JsonWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - nullable: true - array_ends_with: - nullable: true - array_contains: - nullable: true - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedJsonFilter' - _max: - $ref: '#/components/schemas/NestedJsonFilter' - StringFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - IntFieldUpdateOperationsInput: - type: object - properties: - set: - type: integer - increment: - type: integer - decrement: - type: integer - multiply: - type: integer - divide: - type: integer - BigIntFieldUpdateOperationsInput: - type: object - properties: - set: - type: integer - increment: - type: integer - decrement: - type: integer - multiply: - type: integer - divide: - type: integer - DateTimeFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - format: date-time - FloatFieldUpdateOperationsInput: - type: object - properties: - set: - type: number - increment: - type: number - decrement: - type: number - multiply: - type: number - divide: - type: number - DecimalFieldUpdateOperationsInput: - type: object - properties: - set: - oneOf: - - type: string - - type: number - increment: - oneOf: - - type: string - - type: number - decrement: - oneOf: - - type: string - - type: number - multiply: - oneOf: - - type: string - - type: number - divide: - oneOf: - - type: string - - type: number - BoolFieldUpdateOperationsInput: - type: object - properties: - set: - type: boolean - NullableBytesFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - format: byte - nullable: true - NestedStringFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringFilter' - NestedIntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntFilter' - NestedBigIntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedBigIntFilter' - NestedDateTimeFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeFilter' - NestedFloatFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatFilter' - NestedDecimalFilter: - type: object - properties: - equals: - oneOf: - - type: string - - type: number - in: - type: array - items: - oneOf: - - type: string - - type: number - notIn: - type: array - items: - oneOf: - - type: string - - type: number - lt: - oneOf: - - type: string - - type: number - lte: - oneOf: - - type: string - - type: number - gt: - oneOf: - - type: string - - type: number - gte: - oneOf: - - type: string - - type: number - not: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/NestedDecimalFilter' - NestedBoolFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolFilter' - NestedBytesNullableFilter: - type: object - properties: - equals: - type: string - format: byte - nullable: true - in: - type: array - items: - type: string - format: byte - nullable: true - notIn: - type: array - items: - type: string - format: byte - nullable: true - not: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NestedBytesNullableFilter' - nullable: true - NestedStringWithAggregatesFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedStringFilter' - _max: - $ref: '#/components/schemas/NestedStringFilter' - NestedIntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedIntFilter' - _max: - $ref: '#/components/schemas/NestedIntFilter' - NestedBigIntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedBigIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedBigIntFilter' - _min: - $ref: '#/components/schemas/NestedBigIntFilter' - _max: - $ref: '#/components/schemas/NestedBigIntFilter' - NestedDateTimeWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedDateTimeFilter' - _max: - $ref: '#/components/schemas/NestedDateTimeFilter' - NestedFloatWithAggregatesFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedFloatFilter' - _min: - $ref: '#/components/schemas/NestedFloatFilter' - _max: - $ref: '#/components/schemas/NestedFloatFilter' - NestedDecimalWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - type: string - - type: number - in: - type: array - items: - oneOf: - - type: string - - type: number - notIn: - type: array - items: - oneOf: - - type: string - - type: number - lt: - oneOf: - - type: string - - type: number - lte: - oneOf: - - type: string - - type: number - gt: - oneOf: - - type: string - - type: number - gte: - oneOf: - - type: string - - type: number - not: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/NestedDecimalWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedDecimalFilter' - _sum: - $ref: '#/components/schemas/NestedDecimalFilter' - _min: - $ref: '#/components/schemas/NestedDecimalFilter' - _max: - $ref: '#/components/schemas/NestedDecimalFilter' - NestedBoolWithAggregatesFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedBoolFilter' - _max: - $ref: '#/components/schemas/NestedBoolFilter' - NestedBytesNullableWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: byte - nullable: true - in: - type: array - items: - type: string - format: byte - nullable: true - notIn: - type: array - items: - type: string - format: byte - nullable: true - not: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NestedBytesNullableWithAggregatesFilter' - nullable: true - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedBytesNullableFilter' - _max: - $ref: '#/components/schemas/NestedBytesNullableFilter' - NestedIntNullableFilter: - type: object - properties: - equals: - type: integer - nullable: true - in: - type: array - items: - type: integer - nullable: true - notIn: - type: array - items: - type: integer - nullable: true - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntNullableFilter' - nullable: true - NestedJsonNullableFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - nullable: true - array_ends_with: - nullable: true - array_contains: - nullable: true - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - NestedJsonFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - nullable: true - array_ends_with: - nullable: true - array_contains: - nullable: true - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - FooSelect: - type: object - properties: - id: - type: boolean - string: - type: boolean - int: - type: boolean - bigInt: - type: boolean - date: - type: boolean - float: - type: boolean - decimal: - type: boolean - boolean: - type: boolean - bytes: - type: boolean - json: - type: boolean - plainJson: - type: boolean - FooCountAggregateInput: - type: object - properties: - id: - type: boolean - string: - type: boolean - int: - type: boolean - bigInt: - type: boolean - date: - type: boolean - float: - type: boolean - decimal: - type: boolean - boolean: - type: boolean - bytes: - type: boolean - json: - type: boolean - plainJson: - type: boolean - _all: - type: boolean - FooAvgAggregateInput: - type: object - properties: - int: - type: boolean - bigInt: - type: boolean - float: - type: boolean - decimal: - type: boolean - FooSumAggregateInput: - type: object - properties: - int: - type: boolean - bigInt: - type: boolean - float: - type: boolean - decimal: - type: boolean - FooMinAggregateInput: - type: object - properties: - id: - type: boolean - string: - type: boolean - int: - type: boolean - bigInt: - type: boolean - date: - type: boolean - float: - type: boolean - decimal: - type: boolean - boolean: - type: boolean - bytes: - type: boolean - FooMaxAggregateInput: - type: object - properties: - id: - type: boolean - string: - type: boolean - int: - type: boolean - bigInt: - type: boolean - date: - type: boolean - float: - type: boolean - decimal: - type: boolean - boolean: - type: boolean - bytes: - type: boolean - AggregateFoo: - type: object - properties: - _count: - allOf: - - $ref: '#/components/schemas/FooCountAggregateOutputType' - nullable: true - _avg: - allOf: - - $ref: '#/components/schemas/FooAvgAggregateOutputType' - nullable: true - _sum: - allOf: - - $ref: '#/components/schemas/FooSumAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/FooMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/FooMaxAggregateOutputType' - nullable: true - FooGroupByOutputType: - type: object - properties: - id: - type: string - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: string - - type: number - boolean: - type: boolean - bytes: - type: string - format: byte - nullable: true - json: - nullable: true - plainJson: {} - _count: - allOf: - - $ref: '#/components/schemas/FooCountAggregateOutputType' - nullable: true - _avg: - allOf: - - $ref: '#/components/schemas/FooAvgAggregateOutputType' - nullable: true - _sum: - allOf: - - $ref: '#/components/schemas/FooSumAggregateOutputType' - nullable: true - _min: - allOf: - - $ref: '#/components/schemas/FooMinAggregateOutputType' - nullable: true - _max: - allOf: - - $ref: '#/components/schemas/FooMaxAggregateOutputType' - nullable: true - required: - - id - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - plainJson - FooCountAggregateOutputType: - type: object - properties: - id: - type: integer - string: - type: integer - int: - type: integer - bigInt: - type: integer - date: - type: integer - float: - type: integer - decimal: - type: integer - boolean: - type: integer - bytes: - type: integer - json: - type: integer - plainJson: - type: integer - _all: - type: integer - required: - - id - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - - json - - plainJson - - _all - FooAvgAggregateOutputType: - type: object - properties: - int: - type: number - nullable: true - bigInt: - type: number - nullable: true - float: - type: number - nullable: true - decimal: - oneOf: - - type: string - - type: number - nullable: true - FooSumAggregateOutputType: - type: object - properties: - int: - type: integer - nullable: true - bigInt: - type: integer - nullable: true - float: - type: number - nullable: true - decimal: - oneOf: - - type: string - - type: number - nullable: true - FooMinAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - string: - type: string - nullable: true - int: - type: integer - nullable: true - bigInt: - type: integer - nullable: true - date: - type: string - format: date-time - nullable: true - float: - type: number - nullable: true - decimal: - oneOf: - - type: string - - type: number - nullable: true - boolean: - type: boolean - nullable: true - bytes: - type: string - format: byte - nullable: true - FooMaxAggregateOutputType: - type: object - properties: - id: - type: string - nullable: true - string: - type: string - nullable: true - int: - type: integer - nullable: true - bigInt: - type: integer - nullable: true - date: - type: string - format: date-time - nullable: true - float: - type: number - nullable: true - decimal: - oneOf: - - type: string - - type: number - nullable: true - boolean: - type: boolean - nullable: true - bytes: - type: string - format: byte - nullable: true - _Meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Serialization metadata - additionalProperties: true - _Error: - type: object - required: - - error - properties: - error: - type: object - required: - - message - properties: - prisma: - type: boolean - description: Indicates if the error occurred during a Prisma call - rejectedByPolicy: - type: boolean - description: Indicates if the error was due to rejection by a policy - code: - type: string - description: Prisma error code. Only available when "prisma" field is true. - message: - type: string - description: Error message - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - additionalProperties: true - BatchPayload: - type: object - properties: - count: - type: integer - FooCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/FooSelect' - data: - $ref: '#/components/schemas/FooCreateInput' - meta: - $ref: '#/components/schemas/_Meta' - FooCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/FooCreateManyInput' - - type: array - items: - $ref: '#/components/schemas/FooCreateManyInput' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - FooFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - FooFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - FooFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereInput' - orderBy: - oneOf: - - $ref: '#/components/schemas/FooOrderByWithRelationInput' - - type: array - items: - $ref: '#/components/schemas/FooOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/FooWhereUniqueInput' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' - FooUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereUniqueInput' - data: - $ref: '#/components/schemas/FooUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - FooUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/FooWhereInput' - data: - $ref: '#/components/schemas/FooUpdateManyMutationInput' - meta: - $ref: '#/components/schemas/_Meta' - FooUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereUniqueInput' - create: - $ref: '#/components/schemas/FooCreateInput' - update: - $ref: '#/components/schemas/FooUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - FooDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - FooDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/FooWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - FooCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - FooAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/FooWhereInput' - orderBy: - $ref: '#/components/schemas/FooOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/FooWhereUniqueInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/FooCountAggregateInput' - _min: - $ref: '#/components/schemas/FooMinAggregateInput' - _max: - $ref: '#/components/schemas/FooMaxAggregateInput' - _sum: - $ref: '#/components/schemas/FooSumAggregateInput' - _avg: - $ref: '#/components/schemas/FooAvgAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - FooGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/FooWhereInput' - orderBy: - $ref: '#/components/schemas/FooOrderByWithRelationInput' - by: - $ref: '#/components/schemas/FooScalarFieldEnum' - having: - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/FooCountAggregateInput' - _min: - $ref: '#/components/schemas/FooMinAggregateInput' - _max: - $ref: '#/components/schemas/FooMaxAggregateInput' - _sum: - $ref: '#/components/schemas/FooSumAggregateInput' - _avg: - $ref: '#/components/schemas/FooAvgAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' -paths: - /foo/create: - post: - operationId: createFoo - description: Create a new Foo - tags: - - foo - security: [] - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooCreateArgs' - /foo/createMany: - post: - operationId: createManyFoo - description: Create several Foo - tags: - - foo - security: [] - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooCreateManyArgs' - /foo/findUnique: - get: - operationId: findUniqueFoo - description: Find one unique Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/findFirst: - get: - operationId: findFirstFoo - description: Find the first Foo matching the given condition - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/findMany: - get: - operationId: findManyFoo - description: Find a list of Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/update: - patch: - operationId: updateFoo - description: Update a Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooUpdateArgs' - /foo/updateMany: - patch: - operationId: updateManyFoo - description: Update Foos matching the given condition - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooUpdateManyArgs' - /foo/upsert: - post: - operationId: upsertFoo - description: Upsert a Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooUpsertArgs' - /foo/delete: - delete: - operationId: deleteFoo - description: Delete one unique Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/deleteMany: - delete: - operationId: deleteManyFoo - description: Delete Foos matching the given condition - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/count: - get: - operationId: countFoo - description: Find a list of Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/FooCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/aggregate: - get: - operationId: aggregateFoo - description: Aggregate Foos - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateFoo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/groupBy: - get: - operationId: groupByFoo - description: Group Foos by fields - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/FooGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} diff --git a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml deleted file mode 100644 index 5f3e59136..000000000 --- a/packages/plugins/openapi/tests/baseline/rpc-type-coverage-3.1.0.baseline.yaml +++ /dev/null @@ -1,3174 +0,0 @@ -openapi: 3.1.0 -info: - title: ZenStack Generated API - version: 1.0.0 -tags: - - name: foo - description: Foo operations -components: - schemas: - FooScalarFieldEnum: - type: string - enum: - - id - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - - json - - plainJson - SortOrder: - type: string - enum: - - asc - - desc - NullableJsonNullValueInput: - type: string - enum: - - DbNull - - JsonNull - JsonNullValueInput: - type: string - enum: - - JsonNull - QueryMode: - type: string - enum: - - default - - insensitive - JsonNullValueFilter: - type: string - enum: - - DbNull - - JsonNull - - AnyNull - NullsOrder: - type: string - enum: - - first - - last - Foo: - type: object - properties: - id: - type: string - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: string - - type: number - boolean: - type: boolean - bytes: - oneOf: - - type: 'null' - - type: string - format: byte - json: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Meta' - plainJson: {} - required: - - id - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - plainJson - Meta: - type: object - description: The "Meta" TypeDef - properties: - something: - type: string - required: - - something - FooWhereInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/FooWhereInput' - - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/FooWhereInput' - - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - id: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - string: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - int: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - bigInt: - oneOf: - - $ref: '#/components/schemas/BigIntFilter' - - type: integer - date: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - float: - oneOf: - - $ref: '#/components/schemas/FloatFilter' - - type: number - decimal: - oneOf: - - $ref: '#/components/schemas/DecimalFilter' - - oneOf: - - type: string - - type: number - boolean: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - bytes: - oneOf: - - $ref: '#/components/schemas/BytesNullableFilter' - - type: string - format: byte - - type: 'null' - json: - $ref: '#/components/schemas/JsonNullableFilter' - plainJson: - $ref: '#/components/schemas/JsonFilter' - FooOrderByWithRelationInput: - type: object - properties: - id: - $ref: '#/components/schemas/SortOrder' - string: - $ref: '#/components/schemas/SortOrder' - int: - $ref: '#/components/schemas/SortOrder' - bigInt: - $ref: '#/components/schemas/SortOrder' - date: - $ref: '#/components/schemas/SortOrder' - float: - $ref: '#/components/schemas/SortOrder' - decimal: - $ref: '#/components/schemas/SortOrder' - boolean: - $ref: '#/components/schemas/SortOrder' - bytes: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - json: - oneOf: - - $ref: '#/components/schemas/SortOrder' - - $ref: '#/components/schemas/SortOrderInput' - plainJson: - $ref: '#/components/schemas/SortOrder' - FooWhereUniqueInput: - type: object - properties: - id: - type: string - AND: - oneOf: - - $ref: '#/components/schemas/FooWhereInput' - - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - OR: - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - NOT: - oneOf: - - $ref: '#/components/schemas/FooWhereInput' - - type: array - items: - $ref: '#/components/schemas/FooWhereInput' - string: - oneOf: - - $ref: '#/components/schemas/StringFilter' - - type: string - int: - oneOf: - - $ref: '#/components/schemas/IntFilter' - - type: integer - bigInt: - oneOf: - - $ref: '#/components/schemas/BigIntFilter' - - type: integer - date: - oneOf: - - $ref: '#/components/schemas/DateTimeFilter' - - type: string - format: date-time - float: - oneOf: - - $ref: '#/components/schemas/FloatFilter' - - type: number - decimal: - oneOf: - - $ref: '#/components/schemas/DecimalFilter' - - oneOf: - - type: string - - type: number - boolean: - oneOf: - - $ref: '#/components/schemas/BoolFilter' - - type: boolean - bytes: - oneOf: - - $ref: '#/components/schemas/BytesNullableFilter' - - type: string - format: byte - - type: 'null' - json: - $ref: '#/components/schemas/JsonNullableFilter' - plainJson: - $ref: '#/components/schemas/JsonFilter' - FooScalarWhereWithAggregatesInput: - type: object - properties: - AND: - oneOf: - - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - OR: - type: array - items: - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - NOT: - oneOf: - - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - - type: array - items: - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - id: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - string: - oneOf: - - $ref: '#/components/schemas/StringWithAggregatesFilter' - - type: string - int: - oneOf: - - $ref: '#/components/schemas/IntWithAggregatesFilter' - - type: integer - bigInt: - oneOf: - - $ref: '#/components/schemas/BigIntWithAggregatesFilter' - - type: integer - date: - oneOf: - - $ref: '#/components/schemas/DateTimeWithAggregatesFilter' - - type: string - format: date-time - float: - oneOf: - - $ref: '#/components/schemas/FloatWithAggregatesFilter' - - type: number - decimal: - oneOf: - - $ref: '#/components/schemas/DecimalWithAggregatesFilter' - - oneOf: - - type: string - - type: number - boolean: - oneOf: - - $ref: '#/components/schemas/BoolWithAggregatesFilter' - - type: boolean - bytes: - oneOf: - - $ref: '#/components/schemas/BytesNullableWithAggregatesFilter' - - type: string - format: byte - - type: 'null' - json: - $ref: '#/components/schemas/JsonNullableWithAggregatesFilter' - plainJson: - $ref: '#/components/schemas/JsonWithAggregatesFilter' - FooCreateInput: - type: object - properties: - id: - type: string - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: string - - type: number - boolean: - type: boolean - bytes: - oneOf: - - type: 'null' - - type: string - format: byte - json: - oneOf: - - $ref: '#/components/schemas/NullableJsonNullValueInput' - - {} - plainJson: - oneOf: - - $ref: '#/components/schemas/JsonNullValueInput' - - {} - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - plainJson - FooUpdateInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - string: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - int: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - bigInt: - oneOf: - - type: integer - - $ref: '#/components/schemas/BigIntFieldUpdateOperationsInput' - date: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - float: - oneOf: - - type: number - - $ref: '#/components/schemas/FloatFieldUpdateOperationsInput' - decimal: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/DecimalFieldUpdateOperationsInput' - boolean: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - bytes: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NullableBytesFieldUpdateOperationsInput' - - type: 'null' - json: - oneOf: - - $ref: '#/components/schemas/NullableJsonNullValueInput' - - {} - plainJson: - oneOf: - - $ref: '#/components/schemas/JsonNullValueInput' - - {} - FooCreateManyInput: - type: object - properties: - id: - type: string - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: string - - type: number - boolean: - type: boolean - bytes: - oneOf: - - type: 'null' - - type: string - format: byte - json: - oneOf: - - $ref: '#/components/schemas/NullableJsonNullValueInput' - - {} - plainJson: - oneOf: - - $ref: '#/components/schemas/JsonNullValueInput' - - {} - required: - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - plainJson - FooUpdateManyMutationInput: - type: object - properties: - id: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - string: - oneOf: - - type: string - - $ref: '#/components/schemas/StringFieldUpdateOperationsInput' - int: - oneOf: - - type: integer - - $ref: '#/components/schemas/IntFieldUpdateOperationsInput' - bigInt: - oneOf: - - type: integer - - $ref: '#/components/schemas/BigIntFieldUpdateOperationsInput' - date: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/DateTimeFieldUpdateOperationsInput' - float: - oneOf: - - type: number - - $ref: '#/components/schemas/FloatFieldUpdateOperationsInput' - decimal: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/DecimalFieldUpdateOperationsInput' - boolean: - oneOf: - - type: boolean - - $ref: '#/components/schemas/BoolFieldUpdateOperationsInput' - bytes: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NullableBytesFieldUpdateOperationsInput' - - type: 'null' - json: - oneOf: - - $ref: '#/components/schemas/NullableJsonNullValueInput' - - {} - plainJson: - oneOf: - - $ref: '#/components/schemas/JsonNullValueInput' - - {} - StringFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringFilter' - IntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntFilter' - BigIntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedBigIntFilter' - DateTimeFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeFilter' - FloatFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatFilter' - DecimalFilter: - type: object - properties: - equals: - oneOf: - - type: string - - type: number - in: - type: array - items: - oneOf: - - type: string - - type: number - notIn: - type: array - items: - oneOf: - - type: string - - type: number - lt: - oneOf: - - type: string - - type: number - lte: - oneOf: - - type: string - - type: number - gt: - oneOf: - - type: string - - type: number - gte: - oneOf: - - type: string - - type: number - not: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/NestedDecimalFilter' - BoolFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolFilter' - BytesNullableFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: string - format: byte - in: - oneOf: - - type: 'null' - - type: array - items: - type: string - format: byte - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: string - format: byte - not: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NestedBytesNullableFilter' - - type: 'null' - JsonNullableFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - oneOf: - - type: 'null' - - {} - array_ends_with: - oneOf: - - type: 'null' - - {} - array_contains: - oneOf: - - type: 'null' - - {} - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - JsonFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - oneOf: - - type: 'null' - - {} - array_ends_with: - oneOf: - - type: 'null' - - {} - array_contains: - oneOf: - - type: 'null' - - {} - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - SortOrderInput: - type: object - properties: - sort: - $ref: '#/components/schemas/SortOrder' - nulls: - $ref: '#/components/schemas/NullsOrder' - required: - - sort - StringWithAggregatesFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedStringFilter' - _max: - $ref: '#/components/schemas/NestedStringFilter' - IntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedIntFilter' - _max: - $ref: '#/components/schemas/NestedIntFilter' - BigIntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedBigIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedBigIntFilter' - _min: - $ref: '#/components/schemas/NestedBigIntFilter' - _max: - $ref: '#/components/schemas/NestedBigIntFilter' - DateTimeWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedDateTimeFilter' - _max: - $ref: '#/components/schemas/NestedDateTimeFilter' - FloatWithAggregatesFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedFloatFilter' - _min: - $ref: '#/components/schemas/NestedFloatFilter' - _max: - $ref: '#/components/schemas/NestedFloatFilter' - DecimalWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - type: string - - type: number - in: - type: array - items: - oneOf: - - type: string - - type: number - notIn: - type: array - items: - oneOf: - - type: string - - type: number - lt: - oneOf: - - type: string - - type: number - lte: - oneOf: - - type: string - - type: number - gt: - oneOf: - - type: string - - type: number - gte: - oneOf: - - type: string - - type: number - not: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/NestedDecimalWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedDecimalFilter' - _sum: - $ref: '#/components/schemas/NestedDecimalFilter' - _min: - $ref: '#/components/schemas/NestedDecimalFilter' - _max: - $ref: '#/components/schemas/NestedDecimalFilter' - BoolWithAggregatesFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedBoolFilter' - _max: - $ref: '#/components/schemas/NestedBoolFilter' - BytesNullableWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: string - format: byte - in: - oneOf: - - type: 'null' - - type: array - items: - type: string - format: byte - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: string - format: byte - not: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NestedBytesNullableWithAggregatesFilter' - - type: 'null' - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedBytesNullableFilter' - _max: - $ref: '#/components/schemas/NestedBytesNullableFilter' - JsonNullableWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - oneOf: - - type: 'null' - - {} - array_ends_with: - oneOf: - - type: 'null' - - {} - array_contains: - oneOf: - - type: 'null' - - {} - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedJsonNullableFilter' - _max: - $ref: '#/components/schemas/NestedJsonNullableFilter' - JsonWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - oneOf: - - type: 'null' - - {} - array_ends_with: - oneOf: - - type: 'null' - - {} - array_contains: - oneOf: - - type: 'null' - - {} - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedJsonFilter' - _max: - $ref: '#/components/schemas/NestedJsonFilter' - StringFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - IntFieldUpdateOperationsInput: - type: object - properties: - set: - type: integer - increment: - type: integer - decrement: - type: integer - multiply: - type: integer - divide: - type: integer - BigIntFieldUpdateOperationsInput: - type: object - properties: - set: - type: integer - increment: - type: integer - decrement: - type: integer - multiply: - type: integer - divide: - type: integer - DateTimeFieldUpdateOperationsInput: - type: object - properties: - set: - type: string - format: date-time - FloatFieldUpdateOperationsInput: - type: object - properties: - set: - type: number - increment: - type: number - decrement: - type: number - multiply: - type: number - divide: - type: number - DecimalFieldUpdateOperationsInput: - type: object - properties: - set: - oneOf: - - type: string - - type: number - increment: - oneOf: - - type: string - - type: number - decrement: - oneOf: - - type: string - - type: number - multiply: - oneOf: - - type: string - - type: number - divide: - oneOf: - - type: string - - type: number - BoolFieldUpdateOperationsInput: - type: object - properties: - set: - type: boolean - NullableBytesFieldUpdateOperationsInput: - type: object - properties: - set: - oneOf: - - type: 'null' - - type: string - format: byte - NestedStringFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringFilter' - NestedIntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntFilter' - NestedBigIntFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedBigIntFilter' - NestedDateTimeFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeFilter' - NestedFloatFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatFilter' - NestedDecimalFilter: - type: object - properties: - equals: - oneOf: - - type: string - - type: number - in: - type: array - items: - oneOf: - - type: string - - type: number - notIn: - type: array - items: - oneOf: - - type: string - - type: number - lt: - oneOf: - - type: string - - type: number - lte: - oneOf: - - type: string - - type: number - gt: - oneOf: - - type: string - - type: number - gte: - oneOf: - - type: string - - type: number - not: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/NestedDecimalFilter' - NestedBoolFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolFilter' - NestedBytesNullableFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: string - format: byte - in: - oneOf: - - type: 'null' - - type: array - items: - type: string - format: byte - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: string - format: byte - not: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NestedBytesNullableFilter' - - type: 'null' - NestedStringWithAggregatesFilter: - type: object - properties: - equals: - type: string - in: - type: array - items: - type: string - notIn: - type: array - items: - type: string - lt: - type: string - lte: - type: string - gt: - type: string - gte: - type: string - contains: - type: string - startsWith: - type: string - endsWith: - type: string - not: - oneOf: - - type: string - - $ref: '#/components/schemas/NestedStringWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedStringFilter' - _max: - $ref: '#/components/schemas/NestedStringFilter' - NestedIntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedIntFilter' - _max: - $ref: '#/components/schemas/NestedIntFilter' - NestedBigIntWithAggregatesFilter: - type: object - properties: - equals: - type: integer - in: - type: array - items: - type: integer - notIn: - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedBigIntWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedBigIntFilter' - _min: - $ref: '#/components/schemas/NestedBigIntFilter' - _max: - $ref: '#/components/schemas/NestedBigIntFilter' - NestedDateTimeWithAggregatesFilter: - type: object - properties: - equals: - type: string - format: date-time - in: - type: array - items: - type: string - format: date-time - notIn: - type: array - items: - type: string - format: date-time - lt: - type: string - format: date-time - lte: - type: string - format: date-time - gt: - type: string - format: date-time - gte: - type: string - format: date-time - not: - oneOf: - - type: string - format: date-time - - $ref: '#/components/schemas/NestedDateTimeWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedDateTimeFilter' - _max: - $ref: '#/components/schemas/NestedDateTimeFilter' - NestedFloatWithAggregatesFilter: - type: object - properties: - equals: - type: number - in: - type: array - items: - type: number - notIn: - type: array - items: - type: number - lt: - type: number - lte: - type: number - gt: - type: number - gte: - type: number - not: - oneOf: - - type: number - - $ref: '#/components/schemas/NestedFloatWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedFloatFilter' - _sum: - $ref: '#/components/schemas/NestedFloatFilter' - _min: - $ref: '#/components/schemas/NestedFloatFilter' - _max: - $ref: '#/components/schemas/NestedFloatFilter' - NestedDecimalWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - type: string - - type: number - in: - type: array - items: - oneOf: - - type: string - - type: number - notIn: - type: array - items: - oneOf: - - type: string - - type: number - lt: - oneOf: - - type: string - - type: number - lte: - oneOf: - - type: string - - type: number - gt: - oneOf: - - type: string - - type: number - gte: - oneOf: - - type: string - - type: number - not: - oneOf: - - oneOf: - - type: string - - type: number - - $ref: '#/components/schemas/NestedDecimalWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _avg: - $ref: '#/components/schemas/NestedDecimalFilter' - _sum: - $ref: '#/components/schemas/NestedDecimalFilter' - _min: - $ref: '#/components/schemas/NestedDecimalFilter' - _max: - $ref: '#/components/schemas/NestedDecimalFilter' - NestedBoolWithAggregatesFilter: - type: object - properties: - equals: - type: boolean - not: - oneOf: - - type: boolean - - $ref: '#/components/schemas/NestedBoolWithAggregatesFilter' - _count: - $ref: '#/components/schemas/NestedIntFilter' - _min: - $ref: '#/components/schemas/NestedBoolFilter' - _max: - $ref: '#/components/schemas/NestedBoolFilter' - NestedBytesNullableWithAggregatesFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: string - format: byte - in: - oneOf: - - type: 'null' - - type: array - items: - type: string - format: byte - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: string - format: byte - not: - oneOf: - - type: string - format: byte - - $ref: '#/components/schemas/NestedBytesNullableWithAggregatesFilter' - - type: 'null' - _count: - $ref: '#/components/schemas/NestedIntNullableFilter' - _min: - $ref: '#/components/schemas/NestedBytesNullableFilter' - _max: - $ref: '#/components/schemas/NestedBytesNullableFilter' - NestedIntNullableFilter: - type: object - properties: - equals: - oneOf: - - type: 'null' - - type: integer - in: - oneOf: - - type: 'null' - - type: array - items: - type: integer - notIn: - oneOf: - - type: 'null' - - type: array - items: - type: integer - lt: - type: integer - lte: - type: integer - gt: - type: integer - gte: - type: integer - not: - oneOf: - - type: integer - - $ref: '#/components/schemas/NestedIntNullableFilter' - - type: 'null' - NestedJsonNullableFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - oneOf: - - type: 'null' - - {} - array_ends_with: - oneOf: - - type: 'null' - - {} - array_contains: - oneOf: - - type: 'null' - - {} - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - NestedJsonFilter: - type: object - properties: - equals: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - path: - type: array - items: - type: string - mode: - $ref: '#/components/schemas/QueryMode' - string_contains: - type: string - string_starts_with: - type: string - string_ends_with: - type: string - array_starts_with: - oneOf: - - type: 'null' - - {} - array_ends_with: - oneOf: - - type: 'null' - - {} - array_contains: - oneOf: - - type: 'null' - - {} - lt: {} - lte: {} - gt: {} - gte: {} - not: - oneOf: - - {} - - $ref: '#/components/schemas/JsonNullValueFilter' - FooSelect: - type: object - properties: - id: - type: boolean - string: - type: boolean - int: - type: boolean - bigInt: - type: boolean - date: - type: boolean - float: - type: boolean - decimal: - type: boolean - boolean: - type: boolean - bytes: - type: boolean - json: - type: boolean - plainJson: - type: boolean - FooCountAggregateInput: - type: object - properties: - id: - type: boolean - string: - type: boolean - int: - type: boolean - bigInt: - type: boolean - date: - type: boolean - float: - type: boolean - decimal: - type: boolean - boolean: - type: boolean - bytes: - type: boolean - json: - type: boolean - plainJson: - type: boolean - _all: - type: boolean - FooAvgAggregateInput: - type: object - properties: - int: - type: boolean - bigInt: - type: boolean - float: - type: boolean - decimal: - type: boolean - FooSumAggregateInput: - type: object - properties: - int: - type: boolean - bigInt: - type: boolean - float: - type: boolean - decimal: - type: boolean - FooMinAggregateInput: - type: object - properties: - id: - type: boolean - string: - type: boolean - int: - type: boolean - bigInt: - type: boolean - date: - type: boolean - float: - type: boolean - decimal: - type: boolean - boolean: - type: boolean - bytes: - type: boolean - FooMaxAggregateInput: - type: object - properties: - id: - type: boolean - string: - type: boolean - int: - type: boolean - bigInt: - type: boolean - date: - type: boolean - float: - type: boolean - decimal: - type: boolean - boolean: - type: boolean - bytes: - type: boolean - AggregateFoo: - type: object - properties: - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooCountAggregateOutputType' - _avg: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooAvgAggregateOutputType' - _sum: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooSumAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooMaxAggregateOutputType' - FooGroupByOutputType: - type: object - properties: - id: - type: string - string: - type: string - int: - type: integer - bigInt: - type: integer - date: - type: string - format: date-time - float: - type: number - decimal: - oneOf: - - type: string - - type: number - boolean: - type: boolean - bytes: - oneOf: - - type: 'null' - - type: string - format: byte - json: - oneOf: - - type: 'null' - - {} - plainJson: {} - _count: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooCountAggregateOutputType' - _avg: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooAvgAggregateOutputType' - _sum: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooSumAggregateOutputType' - _min: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooMinAggregateOutputType' - _max: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/FooMaxAggregateOutputType' - required: - - id - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - plainJson - FooCountAggregateOutputType: - type: object - properties: - id: - type: integer - string: - type: integer - int: - type: integer - bigInt: - type: integer - date: - type: integer - float: - type: integer - decimal: - type: integer - boolean: - type: integer - bytes: - type: integer - json: - type: integer - plainJson: - type: integer - _all: - type: integer - required: - - id - - string - - int - - bigInt - - date - - float - - decimal - - boolean - - bytes - - json - - plainJson - - _all - FooAvgAggregateOutputType: - type: object - properties: - int: - oneOf: - - type: 'null' - - type: number - bigInt: - oneOf: - - type: 'null' - - type: number - float: - oneOf: - - type: 'null' - - type: number - decimal: - oneOf: - - type: string - - type: number - - type: 'null' - FooSumAggregateOutputType: - type: object - properties: - int: - oneOf: - - type: 'null' - - type: integer - bigInt: - oneOf: - - type: 'null' - - type: integer - float: - oneOf: - - type: 'null' - - type: number - decimal: - oneOf: - - type: string - - type: number - - type: 'null' - FooMinAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - string: - oneOf: - - type: 'null' - - type: string - int: - oneOf: - - type: 'null' - - type: integer - bigInt: - oneOf: - - type: 'null' - - type: integer - date: - oneOf: - - type: 'null' - - type: string - format: date-time - float: - oneOf: - - type: 'null' - - type: number - decimal: - oneOf: - - type: string - - type: number - - type: 'null' - boolean: - oneOf: - - type: 'null' - - type: boolean - bytes: - oneOf: - - type: 'null' - - type: string - format: byte - FooMaxAggregateOutputType: - type: object - properties: - id: - oneOf: - - type: 'null' - - type: string - string: - oneOf: - - type: 'null' - - type: string - int: - oneOf: - - type: 'null' - - type: integer - bigInt: - oneOf: - - type: 'null' - - type: integer - date: - oneOf: - - type: 'null' - - type: string - format: date-time - float: - oneOf: - - type: 'null' - - type: number - decimal: - oneOf: - - type: string - - type: number - - type: 'null' - boolean: - oneOf: - - type: 'null' - - type: boolean - bytes: - oneOf: - - type: 'null' - - type: string - format: byte - _Meta: - type: object - description: Meta information about the request or response - properties: - serialization: - description: Serialization metadata - additionalProperties: true - _Error: - type: object - required: - - error - properties: - error: - type: object - required: - - message - properties: - prisma: - type: boolean - description: Indicates if the error occurred during a Prisma call - rejectedByPolicy: - type: boolean - description: Indicates if the error was due to rejection by a policy - code: - type: string - description: Prisma error code. Only available when "prisma" field is true. - message: - type: string - description: Error message - reason: - type: string - description: Detailed error reason - zodErrors: - type: object - additionalProperties: true - description: Zod validation errors if the error is due to data validation - failure - additionalProperties: true - BatchPayload: - type: object - properties: - count: - type: integer - FooCreateArgs: - type: object - required: - - data - properties: - select: - $ref: '#/components/schemas/FooSelect' - data: - $ref: '#/components/schemas/FooCreateInput' - meta: - $ref: '#/components/schemas/_Meta' - FooCreateManyArgs: - type: object - required: - - data - properties: - data: - oneOf: - - $ref: '#/components/schemas/FooCreateManyInput' - - type: array - items: - $ref: '#/components/schemas/FooCreateManyInput' - skipDuplicates: - type: boolean - description: Do not insert records with unique fields or ID fields that already - exist. - meta: - $ref: '#/components/schemas/_Meta' - FooFindUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - FooFindFirstArgs: - type: object - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - FooFindManyArgs: - type: object - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereInput' - orderBy: - oneOf: - - $ref: '#/components/schemas/FooOrderByWithRelationInput' - - type: array - items: - $ref: '#/components/schemas/FooOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/FooWhereUniqueInput' - take: - type: integer - skip: - type: integer - meta: - $ref: '#/components/schemas/_Meta' - FooUpdateArgs: - type: object - required: - - where - - data - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereUniqueInput' - data: - $ref: '#/components/schemas/FooUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - FooUpdateManyArgs: - type: object - required: - - data - properties: - where: - $ref: '#/components/schemas/FooWhereInput' - data: - $ref: '#/components/schemas/FooUpdateManyMutationInput' - meta: - $ref: '#/components/schemas/_Meta' - FooUpsertArgs: - type: object - required: - - create - - update - - where - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereUniqueInput' - create: - $ref: '#/components/schemas/FooCreateInput' - update: - $ref: '#/components/schemas/FooUpdateInput' - meta: - $ref: '#/components/schemas/_Meta' - FooDeleteUniqueArgs: - type: object - required: - - where - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereUniqueInput' - meta: - $ref: '#/components/schemas/_Meta' - FooDeleteManyArgs: - type: object - properties: - where: - $ref: '#/components/schemas/FooWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - FooCountArgs: - type: object - properties: - select: - $ref: '#/components/schemas/FooSelect' - where: - $ref: '#/components/schemas/FooWhereInput' - meta: - $ref: '#/components/schemas/_Meta' - FooAggregateArgs: - type: object - properties: - where: - $ref: '#/components/schemas/FooWhereInput' - orderBy: - $ref: '#/components/schemas/FooOrderByWithRelationInput' - cursor: - $ref: '#/components/schemas/FooWhereUniqueInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/FooCountAggregateInput' - _min: - $ref: '#/components/schemas/FooMinAggregateInput' - _max: - $ref: '#/components/schemas/FooMaxAggregateInput' - _sum: - $ref: '#/components/schemas/FooSumAggregateInput' - _avg: - $ref: '#/components/schemas/FooAvgAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' - FooGroupByArgs: - type: object - properties: - where: - $ref: '#/components/schemas/FooWhereInput' - orderBy: - $ref: '#/components/schemas/FooOrderByWithRelationInput' - by: - $ref: '#/components/schemas/FooScalarFieldEnum' - having: - $ref: '#/components/schemas/FooScalarWhereWithAggregatesInput' - take: - type: integer - skip: - type: integer - _count: - oneOf: - - type: boolean - - $ref: '#/components/schemas/FooCountAggregateInput' - _min: - $ref: '#/components/schemas/FooMinAggregateInput' - _max: - $ref: '#/components/schemas/FooMaxAggregateInput' - _sum: - $ref: '#/components/schemas/FooSumAggregateInput' - _avg: - $ref: '#/components/schemas/FooAvgAggregateInput' - meta: - $ref: '#/components/schemas/_Meta' -paths: - /foo/create: - post: - operationId: createFoo - description: Create a new Foo - tags: - - foo - security: [] - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooCreateArgs' - /foo/createMany: - post: - operationId: createManyFoo - description: Create several Foo - tags: - - foo - security: [] - responses: - '201': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooCreateManyArgs' - /foo/findUnique: - get: - operationId: findUniqueFoo - description: Find one unique Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooFindUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/findFirst: - get: - operationId: findFirstFoo - description: Find the first Foo matching the given condition - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooFindFirstArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/findMany: - get: - operationId: findManyFoo - description: Find a list of Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooFindManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/update: - patch: - operationId: updateFoo - description: Update a Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooUpdateArgs' - /foo/updateMany: - patch: - operationId: updateManyFoo - description: Update Foos matching the given condition - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooUpdateManyArgs' - /foo/upsert: - post: - operationId: upsertFoo - description: Upsert a Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FooUpsertArgs' - /foo/delete: - delete: - operationId: deleteFoo - description: Delete one unique Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/Foo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooDeleteUniqueArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/deleteMany: - delete: - operationId: deleteManyFoo - description: Delete Foos matching the given condition - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/BatchPayload' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooDeleteManyArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/count: - get: - operationId: countFoo - description: Find a list of Foo - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - oneOf: - - type: integer - - $ref: '#/components/schemas/FooCountAggregateOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooCountArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/aggregate: - get: - operationId: aggregateFoo - description: Aggregate Foos - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/AggregateFoo' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooAggregateArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} - /foo/groupBy: - get: - operationId: groupByFoo - description: Group Foos by fields - tags: - - foo - security: [] - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - data - properties: - data: - type: array - items: - $ref: '#/components/schemas/FooGroupByOutputType' - description: The Prisma response data serialized with superjson - meta: - $ref: '#/components/schemas/_Meta' - description: The superjson serialization metadata for the "data" field - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Invalid request - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is forbidden - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/_Error' - description: Request is unprocessable due to validation errors - parameters: - - name: q - in: query - required: true - description: Superjson-serialized Prisma query object - content: - application/json: - schema: - $ref: '#/components/schemas/FooGroupByArgs' - - name: meta - in: query - description: Superjson serialization metadata for parameter "q" - content: - application/json: - schema: {} diff --git a/packages/plugins/openapi/tests/openapi-restful.test.ts b/packages/plugins/openapi/tests/openapi-restful.test.ts deleted file mode 100644 index bde8ea53c..000000000 --- a/packages/plugins/openapi/tests/openapi-restful.test.ts +++ /dev/null @@ -1,439 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import OpenAPIParser from '@readme/openapi-parser'; -import { getLiteral, getObjectLiteral } from '@zenstackhq/sdk'; -import { Model, Plugin, isPlugin } from '@zenstackhq/sdk/ast'; -import { loadZModelAndDmmf, normalizePath } from '@zenstackhq/testtools'; -import fs from 'fs'; -import path from 'path'; -import * as tmp from 'tmp'; -import YAML from 'yaml'; -import generate from '../src'; - -tmp.setGracefulCleanup(); - -describe('Open API Plugin RESTful Tests', () => { - it('run plugin', async () => { - for (const specVersion of ['3.0.0', '3.1.0']) { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - specVersion = '${specVersion}' -} - -enum role { - USER - ADMIN -} - -model User { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - role role @default(USER) - posts post_Item[] - profile Profile? - likes PostLike[] -} - -model Profile { - id String @id @default(cuid()) - image String? - - user User @relation(fields: [userId], references: [id]) - userId String @unique -} - -model post_Item { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - viewCount Int @default(0) - notes String? - likes PostLike[] - - @@openapi.meta({ - tagDescription: 'Post-related operations' - }) -} - -model PostLike { - post post_Item @relation(fields: [postId], references: [id]) - postId String - user User @relation(fields: [userId], references: [id]) - userId String - @@id([postId, userId]) -} - -model Foo { - id String @id - @@openapi.ignore -} - -model Bar { - id String @id - @@ignore -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output, specVersion); - await generate(model, options, dmmf); - - console.log(`OpenAPI specification generated for ${specVersion}: ${output}`); - - const api = await OpenAPIParser.validate(output); - - expect(api.tags).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'user', description: 'User operations' }), - expect.objectContaining({ name: 'post_Item', description: 'Post-related operations' }), - ]) - ); - - expect(api.paths?.['/user']?.['get']).toBeTruthy(); - expect(api.paths?.['/user']?.['post']).toBeTruthy(); - expect(api.paths?.['/user']?.['put']).toBeFalsy(); - expect(api.paths?.['/user/{id}']?.['get']).toBeTruthy(); - expect(api.paths?.['/user/{id}']?.['patch']).toBeTruthy(); - expect(api.paths?.['/user/{id}']?.['delete']).toBeTruthy(); - expect(api.paths?.['/user/{id}/posts']?.['get']).toBeTruthy(); - expect(api.paths?.['/user/{id}/relationships/posts']?.['get']).toBeTruthy(); - expect(api.paths?.['/user/{id}/relationships/posts']?.['post']).toBeTruthy(); - expect(api.paths?.['/user/{id}/relationships/posts']?.['patch']).toBeTruthy(); - expect(api.paths?.['/user/{id}/relationships/likes']?.['get']).toBeTruthy(); - expect(api.paths?.['/user/{id}/relationships/likes']?.['post']).toBeTruthy(); - expect(api.paths?.['/user/{id}/relationships/likes']?.['patch']).toBeTruthy(); - expect(api.paths?.['/post_Item/{id}/relationships/author']?.['get']).toBeTruthy(); - expect(api.paths?.['/post_Item/{id}/relationships/author']?.['post']).toBeUndefined(); - expect(api.paths?.['/post_Item/{id}/relationships/author']?.['patch']).toBeTruthy(); - expect(api.paths?.['/post_Item/{id}/relationships/likes']?.['get']).toBeTruthy(); - expect(api.paths?.['/post_Item/{id}/relationships/likes']?.['post']).toBeTruthy(); - expect(api.paths?.['/post_Item/{id}/relationships/likes']?.['patch']).toBeTruthy(); - expect(api.paths?.['/foo']).toBeUndefined(); - expect(api.paths?.['/bar']).toBeUndefined(); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe(specVersion); - const baseline = YAML.parse( - fs.readFileSync(`${__dirname}/baseline/rest-${specVersion}.baseline.yaml`, 'utf-8') - ); - expect(parsed).toMatchObject(baseline); - } - }); - - it('common options', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - specVersion = '3.0.0' - title = 'My Awesome API' - version = '1.0.0' - description = 'awesome api' - prefix = '/myapi' -} - -model User { - id String @id -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe('3.0.0'); - - const api = await OpenAPIParser.validate(output); - expect(api.info).toEqual( - expect.objectContaining({ - title: 'My Awesome API', - version: '1.0.0', - description: 'awesome api', - }) - ); - - expect(api.paths?.['/myapi/user']).toBeTruthy(); - }); - - it('security schemes valid', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - securitySchemes = { - myBasic: { type: 'http', scheme: 'basic' }, - myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, - myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' } - } -} - -model User { - id String @id - posts Post[] -} - -model Post { - id String @id - author User @relation(fields: [authorId], references: [id]) - authorId String - @@allow('read', true) -} -`); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.components.securitySchemes).toEqual( - expect.objectContaining({ - myBasic: { type: 'http', scheme: 'basic' }, - myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, - myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' }, - }) - ); - expect(parsed.security).toEqual(expect.arrayContaining([{ myBasic: [] }, { myBearer: [] }])); - - const api = await OpenAPIParser.validate(output); - expect(api.paths?.['/user']?.['get']?.security).toBeUndefined(); - expect(api.paths?.['/user/{id}/posts']?.['get']?.security).toEqual([]); - expect(api.paths?.['/post']?.['get']?.security).toEqual([]); - expect(api.paths?.['/post']?.['post']?.security).toBeUndefined(); - }); - - it('security model level override', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - securitySchemes = { - myBasic: { type: 'http', scheme: 'basic' } - } -} - -model User { - id String @id - value Int - - @@allow('all', value > 0) - - @@openapi.meta({ - security: [] - }) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - const api = await OpenAPIParser.validate(output); - expect(api.paths?.['/user']?.['get']?.security).toHaveLength(0); - expect(api.paths?.['/user/{id}']?.['put']?.security).toHaveLength(0); - }); - - it('security schemes invalid', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - securitySchemes = { - myBasic: { type: 'invalid', scheme: 'basic' } - } -} - -model User { - id String @id -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await expect(generate(model, options, dmmf)).rejects.toEqual( - expect.objectContaining({ message: expect.stringContaining('"securitySchemes" option is invalid') }) - ); - }); - - it('ignored model used as relation', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' -} - -model User { - id String @id - email String @unique - posts Post[] -} - -model Post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - - @@openapi.ignore() -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output, '3.1.0'); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - await OpenAPIParser.validate(output); - }); - - it('field type coverage', async () => { - for (const specVersion of ['3.0.0', '3.1.0']) { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - specVersion = '${specVersion}' -} - -type Meta { - something String -} - -model Foo { - id String @id @default(cuid()) - - string String - int Int - bigInt BigInt - date DateTime - float Float - decimal Decimal - boolean Boolean - bytes Bytes - json Meta? @json - plainJson Json - - @@allow('all', true) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output, specVersion); - await generate(model, options, dmmf); - - console.log(`OpenAPI specification generated for ${specVersion}: ${output}`); - - await OpenAPIParser.validate(output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe(specVersion); - const baseline = YAML.parse( - fs.readFileSync(`${__dirname}/baseline/rest-type-coverage-${specVersion}.baseline.yaml`, 'utf-8') - ); - expect(parsed).toMatchObject(baseline); - } - }); - - it('int field as id', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' -} - -model Foo { - id Int @id @default(autoincrement()) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output, '3.0.0'); - await generate(model, options, dmmf); - console.log(`OpenAPI specification generated: ${output}`); - await OpenAPIParser.validate(output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.components.schemas.Foo.properties.id.type).toBe('integer'); - }); - - it('exposes individual fields from a compound id as attributes', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' -} - -model User { - email String - role String - company String - @@id([role, company]) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output, '3.1.0'); - await generate(model, options, dmmf); - - await OpenAPIParser.validate(output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe('3.1.0'); - - expect(Object.keys(parsed.components.schemas.User.properties.attributes.properties)).toEqual( - expect.arrayContaining(['role', 'company']) - ); - }); - - it('works with mapped model name', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - title = 'My Awesome API' - prefix = '/api' - modelNameMapping = { - User: 'myUser' - } -} - -model User { - id String @id - posts Post[] -} - -model Post { - id String @id - author User @relation(fields: [authorId], references: [id]) - authorId String -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - console.log('OpenAPI specification generated:', output); - const api = await OpenAPIParser.validate(output); - expect(api.paths?.['/api/myUser']).toBeTruthy(); - expect(api.paths?.['/api/user']).toBeFalsy(); - expect(api.paths?.['/api/post']).toBeTruthy(); - }); -}); - -function buildOptions(model: Model, modelFile: string, output: string, specVersion = '3.0.0') { - const optionFields = model.declarations.find((d): d is Plugin => isPlugin(d))?.fields || []; - const options: any = { schemaPath: modelFile, output, specVersion, flavor: 'rest' }; - optionFields.forEach((f) => (options[f.name] = getLiteral(f.value) ?? getObjectLiteral(f.value))); - return options; -} diff --git a/packages/plugins/openapi/tests/openapi-rpc.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts deleted file mode 100644 index 731b38f2a..000000000 --- a/packages/plugins/openapi/tests/openapi-rpc.test.ts +++ /dev/null @@ -1,663 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import OpenAPIParser from '@readme/openapi-parser'; -import { getLiteral, getObjectLiteral } from '@zenstackhq/sdk'; -import { Model, Plugin, isPlugin } from '@zenstackhq/sdk/ast'; -import { loadSchema, loadZModelAndDmmf, normalizePath } from '@zenstackhq/testtools'; -import fs from 'fs'; -import path from 'path'; -import * as tmp from 'tmp'; -import YAML from 'yaml'; -import generate from '../src'; - -tmp.setGracefulCleanup(); - -describe('Open API Plugin RPC Tests', () => { - it('run plugin', async () => { - for (const specVersion of ['3.0.0', '3.1.0']) { - for (const omitInputDetails of [true, false]) { - const { projectDir } = await loadSchema( - ` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - specVersion = '${specVersion}' - omitInputDetails = ${omitInputDetails} - output = '$projectRoot/openapi.yaml' -} - -enum role { - USER - ADMIN -} - -model User { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - role role @default(USER) - posts post_Item[] - profile Profile? - - @@openapi.meta({ - findMany: { - description: 'Find users matching the given conditions' - }, - delete: { - method: 'put', - path: 'dodelete', - description: 'Delete a unique user', - summary: 'Delete a user yeah yeah', - tags: ['delete', 'user'], - deprecated: true - }, - }) -} - -model Profile { - id String @id @default(cuid()) - image String? - - user User @relation(fields: [userId], references: [id]) - userId String @unique -} - -model post_Item { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - viewCount Int @default(0) - notes String? - - @@openapi.meta({ - tagDescription: 'Post-related operations', - findMany: { - ignore: true - } - }) -} - -model Foo { - id String @id - @@openapi.ignore -} - -model Bar { - id String @id - @@ignore -} - `, - { provider: 'postgresql', pushDb: false } - ); - - console.log( - `OpenAPI specification generated for ${specVersion}${ - omitInputDetails ? ' - omit' : '' - }: ${projectDir}/openapi.yaml` - ); - - const parsed = YAML.parse(fs.readFileSync(path.join(projectDir, 'openapi.yaml'), 'utf-8')); - expect(parsed.openapi).toBe(specVersion); - const baseline = YAML.parse( - fs.readFileSync( - `${__dirname}/baseline/rpc-${specVersion}${omitInputDetails ? '-omit' : ''}.baseline.yaml`, - 'utf-8' - ) - ); - expect(parsed).toMatchObject(baseline); - - const api = await OpenAPIParser.validate(path.join(projectDir, 'openapi.yaml')); - - expect(api.tags).toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'user', description: 'User operations' }), - expect.objectContaining({ name: 'post_Item', description: 'Post-related operations' }), - ]) - ); - - expect(api.paths?.['/user/findMany']?.['get']?.description).toBe( - 'Find users matching the given conditions' - ); - const del = api.paths?.['/user/dodelete']?.['put']; - expect(del?.description).toBe('Delete a unique user'); - expect(del?.summary).toBe('Delete a user yeah yeah'); - expect(del?.tags).toEqual(expect.arrayContaining(['delete', 'user'])); - expect(del?.deprecated).toBe(true); - expect(api.paths?.['/post/findMany']).toBeUndefined(); - expect(api.paths?.['/foo/findMany']).toBeUndefined(); - expect(api.paths?.['/bar/findMany']).toBeUndefined(); - } - } - }); - - it('options', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - specVersion = '3.0.0' - title = 'My Awesome API' - version = '1.0.0' - description = 'awesome api' - prefix = '/myapi' -} - -model User { - id String @id -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe('3.0.0'); - - const api = await OpenAPIParser.validate(output); - expect(api.info).toEqual( - expect.objectContaining({ - title: 'My Awesome API', - version: '1.0.0', - description: 'awesome api', - }) - ); - - expect(api.paths?.['/myapi/user/findMany']).toBeTruthy(); - }); - - it('security schemes valid', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - securitySchemes = { - myBasic: { type: 'http', scheme: 'basic' }, - myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, - myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' } - } -} - -model User { - id String @id -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - await OpenAPIParser.validate(output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.components.securitySchemes).toEqual( - expect.objectContaining({ - myBasic: { type: 'http', scheme: 'basic' }, - myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, - myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' }, - }) - ); - expect(parsed.security).toEqual(expect.arrayContaining([{ myBasic: [] }, { myBearer: [] }])); - }); - - it('security schemes invalid', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - securitySchemes = { - myBasic: { type: 'invalid', scheme: 'basic' } - } -} - -model User { - id String @id -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await expect(generate(model, options, dmmf)).rejects.toEqual( - expect.objectContaining({ message: expect.stringContaining('"securitySchemes" option is invalid') }) - ); - }); - - it('security model level override', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - securitySchemes = { - myBasic: { type: 'http', scheme: 'basic' } - } -} - -model User { - id String @id - - @@openapi.meta({ - security: [] - }) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - const api = await OpenAPIParser.validate(output); - expect(api.paths?.['/user/findMany']?.['get']?.security).toHaveLength(0); - }); - - it('security operation level override', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - securitySchemes = { - myBasic: { type: 'http', scheme: 'basic' } - } -} - -model User { - id String @id - - @@allow('read', true) - - @@openapi.meta({ - security: [], - findMany: { - security: [{ myBasic: [] }] - } - }) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - const api = await OpenAPIParser.validate(output); - expect(api.paths?.['/user/findMany']?.['get']?.security).toHaveLength(1); - }); - - it('security inferred', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - securitySchemes = { - myBasic: { type: 'http', scheme: 'basic' } - } -} - -model User { - id String @id - @@allow('create', true) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - const api = await OpenAPIParser.validate(output); - expect(api.paths?.['/user/create']?.['post']?.security).toHaveLength(0); - expect(api.paths?.['/user/findMany']?.['get']?.security).toBeUndefined(); - }); - - it('v3.1.0 fields', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - summary = 'awesome api' -} - -model User { - id String @id -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - await OpenAPIParser.validate(output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe('3.1.0'); - expect(parsed.info.summary).toEqual('awesome api'); - }); - - it('ignored model used as relation', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' -} - -model User { - id String @id - email String @unique - posts Post[] -} - -model Post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - - @@openapi.ignore() -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - await OpenAPIParser.validate(output); - }); - - it('field type coverage', async () => { - for (const specVersion of ['3.0.0', '3.1.0']) { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - specVersion = '${specVersion}' -} - -type Meta { - something String -} - -model Foo { - id String @id @default(cuid()) - - string String - int Int - bigInt BigInt - date DateTime - float Float - decimal Decimal - boolean Boolean - bytes Bytes? - json Meta? @json - plainJson Json - - @@allow('all', true) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log(`OpenAPI specification generated for ${specVersion}: ${output}`); - - await OpenAPIParser.validate(output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe(specVersion); - const baseline = YAML.parse( - fs.readFileSync(`${__dirname}/baseline/rpc-type-coverage-${specVersion}.baseline.yaml`, 'utf-8') - ); - expect(parsed).toMatchObject(baseline); - } - }); - - it('complex TypeDef structures', async () => { - for (const specVersion of ['3.0.0', '3.1.0']) { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - specVersion = '${specVersion}' -} - -enum Status { - PENDING - APPROVED - REJECTED -} - -type Address { - street String - city String - country String - zipCode String? -} - -type ContactInfo { - email String - phone String? - addresses Address[] -} - -type ReviewItem { - id String - status Status - reviewer ContactInfo - score Int - comments String[] - metadata Json? -} - -type ComplexData { - reviews ReviewItem[] - primaryContact ContactInfo - tags String[] - settings Json -} - -model Product { - id String @id @default(cuid()) - name String - data ComplexData @json - simpleJson Json - - @@allow('all', true) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - await OpenAPIParser.validate(output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe(specVersion); - - // Verify all TypeDefs are generated - expect(parsed.components.schemas.Address).toBeDefined(); - expect(parsed.components.schemas.ContactInfo).toBeDefined(); - expect(parsed.components.schemas.ReviewItem).toBeDefined(); - expect(parsed.components.schemas.ComplexData).toBeDefined(); - - // Verify enum reference in TypeDef - expect(parsed.components.schemas.ReviewItem.properties.status.$ref).toBe('#/components/schemas/Status'); - - // Json field inside a TypeDef should remain generic (wrapped with nullable since it's optional) - // OpenAPI 3.1 uses oneOf with null type, while 3.0 uses nullable: true - if (specVersion === '3.1.0') { - expect(parsed.components.schemas.ReviewItem.properties.metadata).toEqual({ - oneOf: [ - { type: 'null' }, - {} - ] - }); - } else { - expect(parsed.components.schemas.ReviewItem.properties.metadata).toEqual({ nullable: true }); - } - - // Verify nested TypeDef references - expect(parsed.components.schemas.ContactInfo.properties.addresses.type).toBe('array'); - expect(parsed.components.schemas.ContactInfo.properties.addresses.items.$ref).toBe('#/components/schemas/Address'); - - // Verify array of complex objects - expect(parsed.components.schemas.ComplexData.properties.reviews.type).toBe('array'); - expect(parsed.components.schemas.ComplexData.properties.reviews.items.$ref).toBe('#/components/schemas/ReviewItem'); - - // Verify the Product model references the ComplexData TypeDef - expect(parsed.components.schemas.Product.properties.data.$ref).toBe('#/components/schemas/ComplexData'); - - // Verify plain Json field remains generic - expect(parsed.components.schemas.Product.properties.simpleJson).toEqual({}); - } - }); - - it('array of TypeDef with enum directly on model field', async () => { - for (const specVersion of ['3.0.0', '3.1.0']) { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - specVersion = '${specVersion}' -} - -enum Language { - FR - EN - ES - DE - IT -} - -type TranslatedField { - language Language - content String -} - -model Article { - id String @id @default(cuid()) - title TranslatedField[] @json - description TranslatedField[] @json - - @@allow('all', true) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - await OpenAPIParser.validate(output); - - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.openapi).toBe(specVersion); - - // Verify TranslatedField TypeDef is generated - expect(parsed.components.schemas.TranslatedField).toBeDefined(); - - // Verify Language enum is generated - expect(parsed.components.schemas.Language).toBeDefined(); - - // Verify enum reference inside TranslatedField - expect(parsed.components.schemas.TranslatedField.properties.language.$ref).toBe('#/components/schemas/Language'); - - // Verify array of TypeDef directly on model field - expect(parsed.components.schemas.Article.properties.title.type).toBe('array'); - expect(parsed.components.schemas.Article.properties.title.items.$ref).toBe('#/components/schemas/TranslatedField'); - - // Verify second array field as well - expect(parsed.components.schemas.Article.properties.description.type).toBe('array'); - expect(parsed.components.schemas.Article.properties.description.items.$ref).toBe('#/components/schemas/TranslatedField'); - } - }); - - it('full-text search', async () => { - const { model, dmmf, modelFile } = await loadZModelAndDmmf(` -generator js { - provider = 'prisma-client-js' - previewFeatures = ['fullTextSearch'] -} - -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' -} - -enum role { - USER - ADMIN -} - -model User { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - role role @default(USER) - posts post_Item[] -} - -model post_Item { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - viewCount Int @default(0) -} - `); - - const { name: output } = tmp.fileSync({ postfix: '.yaml' }); - - const options = buildOptions(model, modelFile, output); - await generate(model, options, dmmf); - - console.log('OpenAPI specification generated:', output); - - await OpenAPIParser.validate(output); - }); - - it('auth() in @default()', async () => { - const { projectDir } = await loadSchema(` -plugin openapi { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/openapi.yaml' - flavor = 'rpc' -} - -model User { - id Int @id - posts Post[] -} - -model Post { - id Int @id - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) -} - `); - - const output = path.join(projectDir, 'openapi.yaml'); - console.log('OpenAPI specification generated:', output); - - await OpenAPIParser.validate(output); - const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); - expect(parsed.components.schemas.PostCreateInput.required).not.toContain('author'); - expect(parsed.components.schemas.PostCreateManyInput.required).not.toContain('authorId'); - }); -}); - -function buildOptions(model: Model, modelFile: string, output: string) { - const optionFields = model.declarations.find((d): d is Plugin => isPlugin(d))?.fields || []; - const options: any = { schemaPath: modelFile, output, flavor: 'rpc' }; - optionFields.forEach((f) => (options[f.name] = getLiteral(f.value) ?? getObjectLiteral(f.value))); - return options; -} diff --git a/packages/plugins/openapi/tsconfig.json b/packages/plugins/openapi/tsconfig.json deleted file mode 100644 index bbb5b304b..000000000 --- a/packages/plugins/openapi/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "strictPropertyInitialization": false - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/plugins/prisma-types.ts b/packages/plugins/prisma-types.ts deleted file mode 100644 index 3e0b8108a..000000000 --- a/packages/plugins/prisma-types.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// Types copied over from Prisma's generated code to avoid being broken due to Prisma upgrades - -export type Enumerable = T | Array; - -type _TupleToUnion = T extends (infer E)[] ? E : never; - -export type TupleToUnion = _TupleToUnion; - -export type MaybeTupleToUnion = T extends any[] ? TupleToUnion : T; - -export type PickEnumerable | keyof T> = Pick>; - -type SelectAndInclude = { - select: any; - include: any; -}; -type HasSelect = { - select: any; -}; -type HasInclude = { - include: any; -}; - -export type CheckSelect = T extends SelectAndInclude - ? 'Please either choose `select` or `include`' - : T extends HasSelect - ? U - : T extends HasInclude - ? U - : S; diff --git a/packages/plugins/swr/CHANGELOG.md b/packages/plugins/swr/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/plugins/swr/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/plugins/swr/LICENSE b/packages/plugins/swr/LICENSE deleted file mode 120000 index 5853aaea5..000000000 --- a/packages/plugins/swr/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../../LICENSE \ No newline at end of file diff --git a/packages/plugins/swr/README.md b/packages/plugins/swr/README.md deleted file mode 100644 index e651fe280..000000000 --- a/packages/plugins/swr/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ZenStack React plugin & runtime - -This package contains ZenStack plugin and runtime for ReactJS. - -Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/plugins/swr/jest.config.ts b/packages/plugins/swr/jest.config.ts deleted file mode 120000 index 09c104090..000000000 --- a/packages/plugins/swr/jest.config.ts +++ /dev/null @@ -1 +0,0 @@ -../../../jest.config.ts \ No newline at end of file diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json deleted file mode 100644 index f1c4cc182..000000000 --- a/packages/plugins/swr/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@zenstackhq/swr", - "displayName": "ZenStack plugin for generating SWR hooks", - "version": "2.22.2", - "description": "ZenStack plugin for generating SWR hooks", - "main": "index.js", - "repository": { - "type": "git", - "url": "https://github.com/zenstackhq/zenstack" - }, - "scripts": { - "clean": "rimraf dist", - "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination ../../../../.build", - "watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\"", - "lint": "eslint src --ext ts", - "test": "jest", - "prepublishOnly": "pnpm build" - }, - "publishConfig": { - "directory": "dist", - "linkDirectory": true - }, - "keywords": [], - "author": "ZenStack Team", - "license": "MIT", - "exports": { - ".": { - "default": "./index.js" - }, - "./package.json": { - "default": "./package.json" - }, - "./runtime": { - "types": "./runtime/index.d.ts", - "import": "./runtime/index.mjs", - "require": "./runtime/index.js", - "default": "./runtime/index.js" - } - }, - "dependencies": { - "@zenstackhq/runtime": "workspace:*", - "@zenstackhq/sdk": "workspace:*", - "cross-fetch": "^4.0.0", - "semver": "^7.5.2", - "ts-morph": "catalog:", - "ts-pattern": "^4.3.0" - }, - "peerDependencies": { - "swr": "2.2.5 - 2" - }, - "devDependencies": { - "@tanstack/react-query": "^4.28.0", - "@testing-library/react": "^14.0.0", - "@types/react": "18.2.0", - "@types/semver": "^7.3.13", - "@types/tmp": "^0.2.3", - "@zenstackhq/testtools": "workspace:*", - "nock": "^13.3.6", - "react": "18.2.0" - } -} diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts deleted file mode 100644 index a2fee939e..000000000 --- a/packages/plugins/swr/src/generator.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { - PluginOptions, - RUNTIME_PACKAGE, - createProject, - ensureEmptyDir, - generateModelMeta, - getDataModels, - isDelegateModel, - requireOption, - resolvePath, - saveProject, -} from '@zenstackhq/sdk'; -import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast'; -import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; -import { upperCaseFirst, paramCase } from '@zenstackhq/runtime/local-helpers'; -import path from 'path'; -import type { OptionalKind, ParameterDeclarationStructure, Project, SourceFile } from 'ts-morph'; -import { P, match } from 'ts-pattern'; -import { name } from '.'; - -export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - let outDir = requireOption(options, 'output', name); - outDir = resolvePath(outDir, options); - ensureEmptyDir(outDir); - - const project = createProject(); - const warnings: string[] = []; - - const models = getDataModels(model); - const typeDefs = model.declarations.filter(isTypeDef); - - await generateModelMeta(project, models, typeDefs, { - output: path.join(outDir, '__model_meta.ts'), - generateAttributes: false, - }); - - generateIndex(project, outDir, models); - - models.forEach((dataModel) => { - const mapping = dmmf.mappings.modelOperations.find((op) => op.model === dataModel.name); - if (!mapping) { - warnings.push(`Unable to find mapping for model ${dataModel.name}`); - return; - } - generateModelHooks(project, outDir, dataModel, mapping, options); - }); - - await saveProject(project); - return { warnings }; -} - -function generateModelHooks( - project: Project, - outDir: string, - model: DataModel, - mapping: DMMF.ModelMapping, - options: PluginOptions -) { - const fileName = paramCase(model.name); - const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); - - const prismaImport = getPrismaClientImportSpec(outDir, options); - sf.addImportDeclaration({ - namedImports: ['Prisma'], - isTypeOnly: true, - moduleSpecifier: prismaImport, - }); - sf.addStatements([ - `import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable } from '@zenstackhq/swr/runtime';`, - `import type { PolicyCrudKind } from '${RUNTIME_PACKAGE}'`, - `import metadata from './__model_meta';`, - `import * as request from '@zenstackhq/swr/runtime';`, - ]); - - const modelNameCap = upperCaseFirst(model.name); - - const mutationFuncs: string[] = []; - - // Note: delegate models don't support create and upsert operations - - // create is somehow named "createOne" in the DMMF - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!isDelegateModel(model) && (mapping.create || (mapping as any).createOne)) { - const argsType = `Prisma.${model.name}CreateArgs`; - mutationFuncs.push(generateMutation(sf, model, 'POST', 'create', argsType, false)); - } - - // createMany - if (!isDelegateModel(model) && mapping.createMany && supportCreateMany(model.$container)) { - const argsType = `Prisma.${model.name}CreateManyArgs`; - mutationFuncs.push(generateMutation(sf, model, 'POST', 'createMany', argsType, true)); - } - - // findMany - if (mapping.findMany) { - const argsType = `Prisma.${model.name}FindManyArgs`; - const inputType = `Prisma.SelectSubset`; - const returnElement = `Prisma.${model.name}GetPayload`; - const returnType = `Array<${returnElement}>`; - const optimisticReturn = `Array<${makeOptimistic(returnElement)}>`; - - // regular findMany - generateQueryHook(sf, model, 'findMany', argsType, inputType, optimisticReturn, undefined, false); - - // infinite findMany - generateQueryHook(sf, model, 'findMany', argsType, inputType, returnType, undefined, true); - } - - // findUnique - if (mapping.findUnique) { - const argsType = `Prisma.${model.name}FindUniqueArgs`; - const inputType = `Prisma.SelectSubset`; - const returnType = makeOptimistic(`Prisma.${model.name}GetPayload`); - generateQueryHook(sf, model, 'findUnique', argsType, inputType, returnType, undefined, false); - } - - // findFirst - if (mapping.findFirst) { - const argsType = `Prisma.${model.name}FindFirstArgs`; - const inputType = `Prisma.SelectSubset`; - const returnType = makeOptimistic(`Prisma.${model.name}GetPayload`); - generateQueryHook(sf, model, 'findFirst', argsType, inputType, returnType, undefined, false); - } - - // update - // update is somehow named "updateOne" in the DMMF - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (mapping.update || (mapping as any).updateOne) { - const argsType = `Prisma.${model.name}UpdateArgs`; - mutationFuncs.push(generateMutation(sf, model, 'PUT', 'update', argsType, false)); - } - - // updateMany - if (mapping.updateMany) { - const argsType = `Prisma.${model.name}UpdateManyArgs`; - mutationFuncs.push(generateMutation(sf, model, 'PUT', 'updateMany', argsType, true)); - } - - // upsert - // upsert is somehow named "upsertOne" in the DMMF - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!isDelegateModel(model) && (mapping.upsert || (mapping as any).upsertOne)) { - const argsType = `Prisma.${model.name}UpsertArgs`; - mutationFuncs.push(generateMutation(sf, model, 'POST', 'upsert', argsType, false)); - } - - // del - // delete is somehow named "deleteOne" in the DMMF - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (mapping.delete || (mapping as any).deleteOne) { - const argsType = `Prisma.${model.name}DeleteArgs`; - mutationFuncs.push(generateMutation(sf, model, 'DELETE', 'delete', argsType, false)); - } - - // deleteMany - if (mapping.deleteMany) { - const argsType = `Prisma.${model.name}DeleteManyArgs`; - mutationFuncs.push(generateMutation(sf, model, 'DELETE', 'deleteMany', argsType, true)); - } - - // aggregate - if (mapping.aggregate) { - const argsType = `Prisma.${modelNameCap}AggregateArgs`; - const inputType = `Prisma.Subset`; - const returnType = `Prisma.Get${modelNameCap}AggregateType`; - generateQueryHook(sf, model, 'aggregate', argsType, inputType, returnType); - } - - // groupBy - if (mapping.groupBy) { - const useName = model.name; - const typeParameters = [ - `T extends Prisma.${useName}GroupByArgs`, - `HasSelectOrTake extends Prisma.Or>, Prisma.Extends<'take', Prisma.Keys>>`, - `OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.${useName}GroupByArgs['orderBy'] }: { orderBy?: Prisma.${useName}GroupByArgs['orderBy'] },`, - `OrderFields extends Prisma.ExcludeUnderscoreKeys>>`, - `ByFields extends Prisma.MaybeTupleToUnion`, - `ByValid extends Prisma.Has`, - `HavingFields extends Prisma.GetHavingFields`, - `HavingValid extends Prisma.Has`, - `ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False`, - `InputErrors extends ByEmpty extends Prisma.True - ? \`Error: "by" must not be empty.\` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? \`Error: Field "\${P}" used in "having" needs to be provided in "by".\` - : [ - Error, - 'Field ', - P, - \` in "having" needs to be provided in "by"\`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields]`, - ]; - const inputType = `Prisma.SubsetIntersection & InputErrors`; - const returnType = `{} extends InputErrors ? - Array & - { - [P in ((keyof T) & (keyof Prisma.${modelNameCap}GroupByOutputType))]: P extends '_count' - ? T[P] extends boolean - ? number - : Prisma.GetScalarType - : Prisma.GetScalarType - } - > : InputErrors`; - generateQueryHook(sf, model, 'groupBy', '', inputType, returnType, typeParameters); - } - - // somehow dmmf doesn't contain "count" operation, so we unconditionally add it here - { - const argsType = `Prisma.${model.name}CountArgs`; - const inputType = `Prisma.Subset`; - const returnType = `T extends { select: any; } ? T['select'] extends true ? number : Prisma.GetScalarType : number`; - generateQueryHook(sf, model, 'count', argsType, inputType, returnType); - } - - // extra `check` hook for ZenStack's permission checker API - { - generateCheckHook(sf, model, prismaImport); - } -} - -function makeOptimistic(returnType: string) { - return `${returnType} & { $optimistic?: boolean }`; -} - -function generateIndex(project: Project, outDir: string, models: DataModel[]) { - const sf = project.createSourceFile(path.join(outDir, 'index.ts'), undefined, { overwrite: true }); - sf.addStatements(models.map((d) => `export * from './${paramCase(d.name)}';`)); - sf.addStatements(`export { Provider } from '@zenstackhq/swr/runtime';`); - sf.addStatements(`export { default as metadata } from './__model_meta';`); -} - -function generateQueryHook( - sf: SourceFile, - model: DataModel, - operation: string, - argsType: string, - inputType: string, - returnType: string, - typeParameters?: string[], - infinite = false -) { - const typeParams = typeParameters ? [...typeParameters] : [`T extends ${argsType}`]; - if (infinite) { - typeParams.push(`R extends ${returnType}`); - } - - const parameters: OptionalKind[] = []; - if (!infinite) { - parameters.push({ - name: 'args?', - type: inputType, - }); - } else { - parameters.push({ - name: 'getNextArgs', - type: `GetNextArgs<${inputType} | undefined, R>`, - }); - } - parameters.push({ - name: 'options?', - type: infinite ? `InfiniteQueryOptions<${returnType}>` : `QueryOptions<${returnType}>`, - }); - - sf.addFunction({ - name: `use${infinite ? 'Infinite' : ''}${upperCaseFirst(operation)}${model.name}`, - typeParameters: typeParams, - isExported: true, - parameters, - }) - .addBody() - .addStatements([ - !infinite - ? `return request.useModelQuery('${model.name}', '${operation}', args, options);` - : `return request.useInfiniteModelQuery('${model.name}', '${operation}', getNextArgs, options);`, - ]); -} - -function generateMutation( - sf: SourceFile, - model: DataModel, - method: 'POST' | 'PUT' | 'PATCH' | 'DELETE', - operation: string, - argsType: string, - batchResult: boolean -) { - // non-batch mutations are subject to read-back check - const checkReadBack = !batchResult; - const genericReturnType = batchResult ? 'Prisma.BatchPayload' : `Prisma.${model.name}GetPayload | undefined`; - const returnType = batchResult ? 'Prisma.BatchPayload' : `Prisma.${model.name}GetPayload<${argsType}> | undefined`; - const genericInputType = `Prisma.SelectSubset`; - - const funcName = `${operation}${model.name}`; - - // generate mutation hook - sf.addFunction({ - name: `use${upperCaseFirst(operation)}${model.name}`, - isExported: true, - parameters: [ - { - name: 'options?', - type: `MutationOptions<${returnType}, unknown, ${argsType}>`, - }, - ], - }) - .addBody() - .addStatements([ - `const mutation = request.useModelMutation('${model.name}', '${method}', '${operation}', metadata, options, ${checkReadBack});`, - `return { - ...mutation, - trigger: (args: ${genericInputType}) => { - return mutation.trigger(args, options as any) as Promise<${genericReturnType}>; - } - };`, - ]); - - return funcName; -} - -function generateCheckHook(sf: SourceFile, model: DataModel, prismaImport: string) { - const mapFilterType = (type: DataModelFieldType) => { - return match(type.type) - .with(P.union('Int', 'BigInt'), () => 'number') - .with('String', () => 'string') - .with('Boolean', () => 'boolean') - .otherwise(() => undefined); - }; - - const filterFields: Array<{ name: string; type: string }> = []; - const enumsToImport = new Set(); - - // collect filterable fields and enums to import - model.fields.forEach((f) => { - if (isEnum(f.type.reference?.ref)) { - enumsToImport.add(f.type.reference.$refText); - filterFields.push({ name: f.name, type: f.type.reference.$refText }); - } - - const mappedType = mapFilterType(f.type); - if (mappedType) { - filterFields.push({ name: f.name, type: mappedType }); - } - }); - - if (enumsToImport.size > 0) { - // import enums - sf.addStatements(`import type { ${Array.from(enumsToImport).join(', ')} } from '${prismaImport}';`); - } - - const whereType = `{ ${filterFields.map(({ name, type }) => `${name}?: ${type}`).join('; ')} }`; - - const func = sf.addFunction({ - name: `useCheck${model.name}`, - isExported: true, - parameters: [ - { name: 'args', type: `{ operation: PolicyCrudKind; where?: ${whereType}; }` }, - { name: 'options?', type: `QueryOptions` }, - ], - }); - - func.addStatements(`return request.useModelQuery('${model.name}', 'check', args, options);`); -} diff --git a/packages/plugins/swr/src/index.ts b/packages/plugins/swr/src/index.ts deleted file mode 100644 index 16ae20c05..000000000 --- a/packages/plugins/swr/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { PluginFunction } from '@zenstackhq/sdk'; -import { generate } from './generator'; - -export const name = 'SWR'; - -const run: PluginFunction = async (model, options, dmmf) => { - if (!dmmf) { - throw new Error('DMMF is required'); - } - return generate(model, options, dmmf); -}; - -export default run; diff --git a/packages/plugins/swr/src/runtime/index.ts b/packages/plugins/swr/src/runtime/index.ts deleted file mode 100644 index 9f1df0b8e..000000000 --- a/packages/plugins/swr/src/runtime/index.ts +++ /dev/null @@ -1,552 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { deserialize, serialize } from '@zenstackhq/runtime/browser'; -import { - applyMutation, - getMutatedModels, - getReadModels, - type ModelMeta, - type PrismaWriteActionType, -} from '@zenstackhq/runtime/cross'; -import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { createContext, useContext } from 'react'; -import type { Cache, Fetcher, SWRConfiguration, SWRResponse } from 'swr'; -import useSWR, { useSWRConfig } from 'swr'; -import { ScopedMutator } from 'swr/_internal'; -import useSWRInfinite, { - unstable_serialize, - type SWRInfiniteConfiguration, - type SWRInfiniteFetcher, - type SWRInfiniteResponse, -} from 'swr/infinite'; -import useSWRMutation, { type SWRMutationConfiguration } from 'swr/mutation'; -export * from './prisma-types'; - -/** - * Function signature for `fetch`. - */ -export type FetchFn = (url: string, options?: RequestInit) => Promise; - -/** - * Context type for configuring react hooks. - */ -export type RequestHandlerContext = { - /** - * The endpoint to use for the queries. - */ - endpoint?: string; - - /** - * A custom fetch function for sending the HTTP requests. - */ - fetch?: FetchFn; - - /** - * If logging is enabled. - */ - logging?: boolean; -}; - -const DEFAULT_QUERY_ENDPOINT = '/api/model'; - -/** - * Context for configuring react hooks. - */ -export const RequestHandlerContext = createContext({ - endpoint: DEFAULT_QUERY_ENDPOINT, - fetch: undefined, -}); - -/** - * Context provider. - */ -export const Provider = RequestHandlerContext.Provider; - -/** - * Hooks context. - */ -export function useHooksContext() { - const { endpoint, ...rest } = useContext(RequestHandlerContext); - return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; -} - -/** - * Regular query options. - */ -export type QueryOptions = { - /** - * Disable data fetching - */ - disabled?: boolean; - - /** - * Whether to enable automatic optimistic update. Defaults to `true`. - */ - optimisticUpdate?: boolean; -} & Omit>, 'fetcher'>; - -/** - * Infinite query options. - */ -export type InfiniteQueryOptions = { - /** - * Disable data fetching - */ - disabled?: boolean; -} & Omit>, 'fetcher'>; - -const QUERY_KEY_PREFIX = 'zenstack:query'; -const MUTATION_KEY_PREFIX = 'zenstack:mutation'; - -type QueryKey = { - prefix: typeof QUERY_KEY_PREFIX; - model: string; - operation: string; - args?: unknown; - infinite?: boolean; - optimisticUpdate?: boolean; -}; - -/** - * Result of optimistic data provider. - */ -export type OptimisticDataProviderResult = { - /** - * Kind of the result. - * - Update: use the `data` field to update the query cache. - * - Skip: skip the optimistic update for this query. - * - ProceedDefault: proceed with the default optimistic update. - */ - kind: 'Update' | 'Skip' | 'ProceedDefault'; - - /** - * Data to update the query cache. Only applicable if `kind` is 'Update'. - * - * If the data is an object with fields updated, it should have a `$optimistic` - * field set to `true`. If it's an array and an element object is created or updated, - * the element should have a `$optimistic` field set to `true`. - */ - data?: any; -}; - -/** - * Optimistic data provider. - * - * @param args Arguments. - * @param args.queryModel The model of the query. - * @param args.queryOperation The operation of the query, `findMany`, `count`, etc. - * @param args.queryArgs The arguments of the query. - * @param args.currentData The current cache data for the query. - * @param args.mutationArgs The arguments of the mutation. - */ -export type OptimisticDataProvider = (args: { - queryModel: string; - queryOperation: string; - queryArgs: any; - currentData: any; - mutationArgs: any; -}) => OptimisticDataProviderResult | Promise; - -/** - * Mutation options. - */ -export type MutationOptions = { - /** - * Whether to automatically optimistic-update queries potentially impacted. Defaults to `false`. - */ - optimisticUpdate?: boolean; - - /** - * A callback for computing optimistic update data for each query cache entry. - */ - optimisticDataProvider?: OptimisticDataProvider; -} & Omit, 'fetcher'>; - -/** - * Computes query key for the given model, operation, query args, and options. - */ -export function getQueryKey( - model: string, - operation: string, - args?: unknown, - infinite?: boolean, - optimisticUpdate?: boolean -) { - return JSON.stringify({ - prefix: QUERY_KEY_PREFIX, - model, - operation, - args, - infinite: infinite === true, - optimisticUpdate: optimisticUpdate !== false, - }); -} - -function getMutationKey(model: string, operation: string) { - // use a random key since we don't have 1:1 mapping between mutation and query - // https://github.com/vercel/swr/discussions/2461#discussioncomment-5281784 - return JSON.stringify({ prefix: MUTATION_KEY_PREFIX, model, operation, r: Date.now() }); -} - -function parseQueryKey(key: unknown): QueryKey | undefined { - let keyValue: any = key; - if (typeof key === 'string') { - try { - keyValue = JSON.parse(key); - } catch { - return undefined; - } - } - return keyValue?.prefix === QUERY_KEY_PREFIX ? (keyValue as QueryKey) : undefined; -} - -/** - * Makes a model query with SWR. - * - * @param model Model name - * @param operation Prisma operation (e.g, `findMany`) - * @param args The request args object, which will be superjson-stringified and appended as "?q=" parameter - * @param options Query options - * @returns SWR response - */ -export function useModelQuery( - model: string, - operation: string, - args?: unknown, - options?: QueryOptions -): SWRResponse { - const { endpoint, fetch } = useHooksContext(); - const key = options?.disabled - ? null - : getQueryKey(model, operation, args, false, options?.optimisticUpdate !== false); - const url = makeUrl(`${endpoint}/${lowerCaseFirst(model)}/${operation}`, args); - return useSWR(key, () => fetcher(url, undefined, fetch, false), options); -} - -/** - * Function for computing the query args for fetching a page during an infinite query. - */ -export type GetNextArgs = (pageIndex: number, previousPageData: Result | null) => Args | null; - -/** - * Makes an infinite GET request with SWR. - * - * @param model Model name - * @param operation Prisma operation (e.g, `findMany`) - * @param getNextArgs Function for computing the query args for a page - * @param options Query options - * @returns SWR infinite query response - */ -export function useInfiniteModelQuery( - model: string, - operation: string, - getNextArgs: GetNextArgs, - options?: InfiniteQueryOptions -): SWRInfiniteResponse { - const { endpoint, fetch } = useHooksContext(); - - const getKey = (pageIndex: number, previousPageData: Result | null) => { - if (options?.disabled) { - return null; - } - const nextArgs = getNextArgs(pageIndex, previousPageData); - return nextArgs !== null // null means reached the end - ? getQueryKey(model, operation, nextArgs, true, false) - : null; - }; - - return useSWRInfinite( - getKey, - (key: unknown) => { - const parsedKey = parseQueryKey(key); - if (parsedKey) { - const { model, operation, args } = parsedKey; - const url = makeUrl(`${endpoint}/${lowerCaseFirst(model)}/${operation}`, args); - return fetcher(url, undefined, fetch, false); - } else { - throw new Error('Invalid query key: ' + key); - } - }, - options - ); -} - -export function useModelMutation( - model: string, - method: 'POST' | 'PUT' | 'DELETE', - operation: string, - modelMeta: ModelMeta, - options?: MutationOptions, - checkReadBack?: CheckReadBack -) { - const { endpoint, fetch, logging } = useHooksContext(); - const invalidate = options?.revalidate !== false ? useInvalidation(model, modelMeta) : undefined; - const { cache, mutate } = useSWRConfig(); - - return useSWRMutation( - getMutationKey(model, operation), - (_key, { arg }: { arg: any }) => { - if (options?.optimisticUpdate) { - optimisticUpdate(model, operation, arg, options, modelMeta, cache, mutate, logging); - } - const url = `${endpoint}/${lowerCaseFirst(model)}/${operation}`; - return mutationRequest(method, url, arg, invalidate, fetch, checkReadBack); - }, - options - ); -} - -/** - * Makes a mutation request. - * - * @param url The request URL - * @param data The request data - * @param invalidate Function for invalidating a query - */ -export async function mutationRequest( - method: 'POST' | 'PUT' | 'DELETE', - url: string, - data: unknown, - invalidate?: Invalidator, - fetch?: FetchFn, - checkReadBack?: C -): Promise { - const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; - const r = await fetcher( - reqUrl, - { - method, - headers: { - 'content-type': 'application/json', - }, - body: data ? marshal(data) : undefined, - }, - fetch, - checkReadBack - ); - - if (invalidate) { - await invalidate(getOperationFromUrl(url), data); - } - return r; -} - -// function for invalidating queries related to mutation represented by its operation and args -type Invalidator = (operation: string, args?: unknown) => ReturnType; - -export function useInvalidation(model: string, modelMeta: ModelMeta): Invalidator { - // https://swr.vercel.app/docs/advanced/cache#mutate-multiple-keys-from-regex - const { logging } = useHooksContext(); - const { cache, mutate } = useSWRConfig(); - return async (operation: string, args: unknown) => { - if (!(cache instanceof Map)) { - throw new Error('mutate requires the cache provider to be a Map instance'); - } - - const mutatedModels = await getMutatedModels(model, operation as PrismaWriteActionType, args, modelMeta); - - const keys = Array.from(cache.keys()).filter((key: unknown) => { - const parsedKey = parseQueryKey(key); - if (!parsedKey) { - return false; - } - const modelsRead = getReadModels(parsedKey.model, modelMeta, parsedKey.args); - return modelsRead.some((m) => mutatedModels.includes(m)); - }); - - if (logging) { - keys.forEach((key) => { - console.log(`Invalidating query ${key} due to mutation "${model}.${operation}"`); - }); - } - - const mutations = keys.map((key) => { - const parsedKey = parseQueryKey(key); - // FIX: special handling for infinite query keys, but still not working - // https://github.com/vercel/swr/discussions/2843 - return mutate(parsedKey?.infinite ? unstable_serialize(() => key) : key); - }); - return Promise.all(mutations); - }; -} - -/** - * Makes fetch request for queries and mutations. - */ -export async function fetcher( - url: string, - options?: RequestInit, - customFetch?: FetchFn, - checkReadBack?: C -): Promise { - // Note: 'cross-fetch' is supposed to handle fetch compatibility - // but it doesn't work for cloudflare workers - const _fetch = - customFetch ?? - // check if fetch is available globally - (typeof fetch === 'function' - ? fetch - : // fallback to 'cross-fetch' if otherwise - (await import('cross-fetch')).default); - - const res = await _fetch(url, options); - if (!res.ok) { - const errData = unmarshal(await res.text()); - if ( - checkReadBack !== false && - errData.error?.prisma && - errData.error?.code === 'P2004' && - errData.error?.reason === 'RESULT_NOT_READABLE' - ) { - // policy doesn't allow mutation result to be read back, just return undefined - return undefined as any; - } - const error: Error & { info?: unknown; status?: number } = new Error( - 'An error occurred while fetching the data.' - ); - error.info = errData.error; - error.status = res.status; - throw error; - } - - const textResult = await res.text(); - try { - return unmarshal(textResult).data as R; - } catch (err) { - console.error(`Unable to deserialize data:`, textResult); - throw err; - } -} - -function marshal(value: unknown) { - const { data, meta } = serialize(value); - if (meta) { - return JSON.stringify({ ...(data as any), meta: { serialization: meta } }); - } else { - return JSON.stringify(data); - } -} - -function unmarshal(value: string) { - const parsed = JSON.parse(value); - if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) { - const deserializedData = deserialize(parsed.data, parsed.meta.serialization); - return { ...parsed, data: deserializedData }; - } else { - return parsed; - } -} - -function makeUrl(url: string, args: unknown) { - if (!args) { - return url; - } - - const { data, meta } = serialize(args); - let result = `${url}?q=${encodeURIComponent(JSON.stringify(data))}`; - if (meta) { - result += `&meta=${encodeURIComponent(JSON.stringify({ serialization: meta }))}`; - } - return result; -} - -function getOperationFromUrl(url: string) { - const parts = url.split('/'); - const r = parts.pop(); - if (!r) { - throw new Error(`Invalid URL: ${url}`); - } else { - return r; - } -} - -async function optimisticUpdate( - mutationModel: string, - mutationOp: string, - mutationArgs: any, - options: MutationOptions | undefined, - modelMeta: ModelMeta, - cache: Cache, - mutator: ScopedMutator, - logging = false -) { - const optimisticPromises: Array> = []; - for (const key of cache.keys()) { - const parsedKey = parseQueryKey(key); - if (!parsedKey) { - continue; - } - - if (!parsedKey.optimisticUpdate) { - if (logging) { - console.log(`Skipping optimistic update for ${key} due to opt-out`); - } - continue; - } - - const cacheValue = cache.get(key); - if (cacheValue?.error) { - if (logging) { - console.warn(`Skipping optimistic update for ${key} due to error:`, cacheValue.error); - } - continue; - } - - if (options?.optimisticDataProvider) { - const providerResult = await options.optimisticDataProvider({ - queryModel: parsedKey.model, - queryOperation: parsedKey.operation, - queryArgs: parsedKey.args, - currentData: cacheValue?.data, - mutationArgs, - }); - - if (providerResult?.kind === 'Skip') { - if (logging) { - console.log(`Skipping optimistic update for ${key} due to custom provider`); - } - continue; - } else if (providerResult?.kind === 'Update') { - if (logging) { - console.log(`Optimistically updating query ${JSON.stringify(key)} due to provider`); - } - optimisticPromises.push(mutator(key, providerResult.data, { revalidate: false })); - continue; - } - } - - if (!cacheValue) { - continue; - } - - const mutatedData = await applyMutation( - parsedKey.model, - parsedKey.operation, - cacheValue.data, - mutationModel, - mutationOp as PrismaWriteActionType, - mutationArgs, - modelMeta, - logging - ); - - if (mutatedData !== undefined) { - // mutation applicable to this query, update cache - if (logging) { - console.log( - `Optimistically updating query ${JSON.stringify( - key - )} due to mutation "${mutationModel}.${mutationOp}"` - ); - } - optimisticPromises.push( - mutator(key, mutatedData, { - // don't trigger revalidation here since we will do it - // when the remote mutation succeeds - revalidate: false, - }) - ); - } - } - - return Promise.all(optimisticPromises); -} diff --git a/packages/plugins/swr/src/runtime/prisma-types.ts b/packages/plugins/swr/src/runtime/prisma-types.ts deleted file mode 120000 index 277b4b133..000000000 --- a/packages/plugins/swr/src/runtime/prisma-types.ts +++ /dev/null @@ -1 +0,0 @@ -../../../prisma-types.ts \ No newline at end of file diff --git a/packages/plugins/swr/tests/react-hooks.test.tsx b/packages/plugins/swr/tests/react-hooks.test.tsx deleted file mode 100644 index 78f41a40b..000000000 --- a/packages/plugins/swr/tests/react-hooks.test.tsx +++ /dev/null @@ -1,755 +0,0 @@ -/** - * @jest-environment jsdom - */ - -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import { renderHook, waitFor } from '@testing-library/react'; -import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import nock from 'nock'; -import { SWRConfig, useSWRConfig } from 'swr'; -import { - RequestHandlerContext, - getQueryKey, - mutationRequest, - useModelQuery, - useInvalidation, - useModelMutation, -} from '../src/runtime'; -import { modelMeta } from './test-model-meta'; -import React from 'react'; - -const ENDPOINT = 'http://localhost/api/model'; - -function makeUrl(model: string, operation: string, args?: unknown) { - let r = `${ENDPOINT}/${lowerCaseFirst(model)}/${operation}`; - if (args) { - r += `?q=${encodeURIComponent(JSON.stringify(args))}`; - } - return r; -} - -describe('SWR React Hooks Test', () => { - // let cache: Map; - const wrapper = ({ children }: { children: React.ReactNode }) => ( - - - {children} - - - ); - - beforeEach(async () => { - nock.cleanAll(); - const { result } = renderHook(() => useSWRConfig()); - await waitFor(async () => { - await result.current.mutate(() => true, undefined, { revalidate: false }); - }); - // await result.current.mutate(() => true, undefined, { revalidate: false }); - }); - - it('simple query', async () => { - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Query data:', data); - return { - data, - }; - }); - - const { result } = renderHook(() => useModelQuery('User', 'findUnique', queryArgs), { wrapper }); - - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData?.data).toMatchObject(data); - }); - }); - - it('create and invalidation legacy', async () => { - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findMany'), { wrapper }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '1', name: 'foo' }); - return { data: data[0] }; - }); - - const { result: invalidationResult } = renderHook(() => useInvalidation('User', modelMeta)); - - await waitFor(async () => { - const mutate = invalidationResult.current; - const r = await mutationRequest( - 'POST', - makeUrl('User', 'create', undefined), - { data: { name: 'foo' } }, - mutate - ); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findMany', undefined)); - expect(cacheData?.data).toHaveLength(1); - }); - }); - - it('create and invalidation hooks with invalidation', async () => { - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findMany'), { wrapper }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '1', name: 'foo' }); - return { data: data[0] }; - }); - - const { result: useMutateResult } = renderHook(() => useModelMutation('User', 'POST', 'create', modelMeta), { - wrapper, - }); - - await waitFor(async () => { - const { trigger } = useMutateResult.current; - const r = await trigger({ data: { name: 'foo' } }); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findMany', undefined)); - expect(cacheData?.data).toHaveLength(1); - }); - }); - - it('create and invalidation hooks no invalidation', async () => { - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findMany'), { wrapper }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '1', name: 'foo' }); - return { data: data[0] }; - }); - - const { result: useMutateResult } = renderHook( - () => useModelMutation('User', 'POST', 'create', modelMeta, { revalidate: false }), - { - wrapper, - } - ); - - await waitFor(async () => { - const { trigger } = useMutateResult.current; - const r = await trigger({ data: { name: 'foo' } }); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findMany', undefined)); - expect(cacheData?.data).toHaveLength(0); - }); - }); - - it('update and invalidation legacy', async () => { - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findUnique', queryArgs), { wrapper }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.name = 'bar'; - return data; - }); - - const { result: useMutateResult } = renderHook(() => useInvalidation('User', modelMeta)); - - await waitFor(async () => { - const mutate = useMutateResult.current; - const r = await mutationRequest( - 'PUT', - makeUrl('User', 'update', undefined), - { ...queryArgs, data: { name: 'bar' } }, - mutate - ); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData?.data).toMatchObject({ name: 'bar' }); - }); - }); - - it('update and invalidation hooks', async () => { - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findUnique', queryArgs), { wrapper }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.name = 'bar'; - return data; - }); - - const { result: useMutateResult } = renderHook(() => useModelMutation('User', 'PUT', 'update', modelMeta), { - wrapper, - }); - - await waitFor(async () => { - const { trigger } = useMutateResult.current; - const r = await trigger({ ...queryArgs, data: { name: 'bar' } }); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData?.data).toMatchObject({ name: 'bar' }); - }); - }); - - it('top-level mutation and nested-read invalidation', async () => { - const queryArgs = { where: { id: '1' }, include: { posts: true } }; - const data = { posts: [{ id: '1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findUnique', queryArgs), { wrapper }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('Post', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.posts[0].title = 'post2'; - return data; - }); - - const { result: useMutateResult } = renderHook(() => useInvalidation('Post', modelMeta)); - - await waitFor(async () => { - const mutate = useMutateResult.current; - const r = await mutationRequest( - 'PUT', - makeUrl('Post', 'update', undefined), - { where: { id: '1' }, data: { name: 'post2' } }, - mutate - ); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData?.data.posts[0].title).toBe('post2'); - }); - }); - - it('nested mutation and top-level-read invalidation', async () => { - const data = [{ id: '1', title: 'post1', ownerId: '1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('Post', 'findMany'), { wrapper }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '2', title: 'post2', ownerId: '1' }); - return data; - }); - - const { result: useMutateResult } = renderHook(() => useInvalidation('User', modelMeta)); - - await waitFor(async () => { - const mutate = useMutateResult.current; - const r = await mutationRequest( - 'PUT', - makeUrl('User', 'update', undefined), - { where: { id: '1' }, data: { posts: { create: { title: 'post2' } } } }, - mutate - ); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('Post', 'findMany')); - expect(cacheData?.data).toHaveLength(2); - }); - }); - - it('independent mutation and query', async () => { - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - let queryCount = 0; - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - queryCount++; - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findUnique', queryArgs), { wrapper }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('Post', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - return { data: { id: '1', title: 'post1' } }; - }); - - const { result: useMutateResult } = renderHook(() => useInvalidation('Post', modelMeta)); - await waitFor(async () => { - const mutate = useMutateResult.current; - const r = await mutationRequest( - 'POST', - makeUrl('Post', 'create', undefined), - { data: { title: 'post1' } }, - mutate - ); - console.log('Mutate result:', r); - // no refetch caused by invalidation - expect(queryCount).toBe(1); - }); - }); - - it('cascaded delete', async () => { - const data: any[] = [{ id: '1', title: 'post1', ownerId: '1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('Post', 'findMany'), { wrapper }); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.pop(); - return { data: { id: '1' } }; - }); - - const { result: useMutateResult } = renderHook(() => useInvalidation('User', modelMeta)); - - await waitFor(async () => { - const mutate = useMutateResult.current; - await mutationRequest('DELETE', makeUrl('User', 'delete', undefined), { where: { id: '1' } }, mutate); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('Post', 'findMany')); - expect(cacheData?.data).toHaveLength(0); - }); - }); - - it('optimistic create single', async () => { - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findMany'), { wrapper }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: useMutateResult } = renderHook( - () => useModelMutation('User', 'POST', 'create', modelMeta, { optimisticUpdate: true, revalidate: false }), - { - wrapper, - } - ); - - await waitFor(async () => { - const { trigger } = useMutateResult.current; - const r = await trigger({ data: { name: 'foo' } }); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findMany')); - expect(cacheData?.data).toHaveLength(1); - }); - }); - - it('optimistic create many', async () => { - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findMany'), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'createMany')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'POST', 'createMany', modelMeta, { - optimisticUpdate: true, - revalidate: false, - }), - { - wrapper, - } - ); - - await waitFor(async () => { - const { trigger } = mutationResult.current; - await trigger({ data: [{ name: 'foo' }, { name: 'bar' }] }); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findMany')); - expect(cacheData?.data).toHaveLength(2); - }); - }); - - it('optimistic update', async () => { - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findUnique', queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return data; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'PUT', 'update', modelMeta, { - optimisticUpdate: true, - revalidate: false, - }), - { - wrapper, - } - ); - - await waitFor(async () => { - await mutationResult.current.trigger({ ...queryArgs, data: { name: 'bar' } }); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData?.data).toMatchObject({ name: 'bar', $optimistic: true }); - }); - }); - - it('optimistic delete', async () => { - const data: any[] = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findMany'), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'DELETE', 'delete', modelMeta, { - optimisticUpdate: true, - revalidate: false, - }), - { - wrapper, - } - ); - - await waitFor(async () => await mutationResult.current.trigger({ where: { id: '1' } })); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findMany')); - expect(cacheData?.data).toHaveLength(0); - }); - }); - - it('optimistic create with custom provider', async () => { - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', 'findMany'), { wrapper }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }) - .persist(); - - const { result: useMutateResult1 } = renderHook( - () => - useModelMutation('User', 'POST', 'create', modelMeta, { - optimisticUpdate: true, - revalidate: false, - optimisticDataProvider: ({ queryModel, queryOperation }) => { - if (queryModel === 'User' && queryOperation === 'findMany') { - return { kind: 'Skip' }; - } else { - return { kind: 'ProceedDefault' }; - } - }, - }), - { - wrapper, - } - ); - - await waitFor(async () => { - const { trigger } = useMutateResult1.current; - const r = await trigger({ data: { name: 'foo' } }); - console.log('Mutate result:', r); - }); - - const { result: cacheResult1 } = renderHook(() => useSWRConfig()); - // cache should not update - await waitFor(() => { - const cache = cacheResult1.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findMany')); - expect(cacheData?.data).toHaveLength(0); - }); - - const { result: useMutateResult2 } = renderHook( - () => - useModelMutation('User', 'POST', 'create', modelMeta, { - optimisticUpdate: true, - revalidate: false, - optimisticDataProvider: ({ queryModel, queryOperation, currentData, mutationArgs }) => { - if (queryModel === 'User' && queryOperation === 'findMany') { - return { - kind: 'Update', - data: [ - ...currentData, - { id: 100, name: mutationArgs.data.name + 'hooray', $optimistic: true }, - ], - }; - } else { - return { kind: 'ProceedDefault' }; - } - }, - }), - { - wrapper, - } - ); - - await waitFor(async () => { - const { trigger } = useMutateResult2.current; - const r = await trigger({ data: { name: 'foo' } }); - console.log('Mutate result:', r); - }); - - const { result: cacheResult } = renderHook(() => useSWRConfig()); - // cache should update - await waitFor(() => { - const cache = cacheResult.current.cache; - const cacheData = cache.get(getQueryKey('User', 'findMany')); - expect(cacheData?.data).toHaveLength(1); - expect(cacheData?.data[0].name).toBe('foohooray'); - }); - }); -}); diff --git a/packages/plugins/swr/tests/swr.test.ts b/packages/plugins/swr/tests/swr.test.ts deleted file mode 100644 index 7c26f18bd..000000000 --- a/packages/plugins/swr/tests/swr.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -/// - -import { loadSchema, normalizePath } from '@zenstackhq/testtools'; -import fs from 'fs'; -import path from 'path'; -import tmp from 'tmp'; - -describe('SWR Plugin Tests', () => { - let origDir: string; - - beforeAll(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - const sharedModel = ` -model User { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - role role @default(USER) - posts post_Item[] -} - -enum role { - USER - ADMIN -} - -model post_Item { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - viewCount Int @default(0) -} - -model Foo { - id String @id - @@ignore -} - `; - - it('run plugin', async () => { - await loadSchema( - ` -plugin swr { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' -} - -${sharedModel} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: [ - `${normalizePath(path.join(__dirname, '../dist'))}`, - 'react@18.2.0', - '@types/react@18.2.0', - 'swr@^2', - ], - compile: true, - } - ); - }); - - it('clear output', async () => { - const { name: projectDir } = tmp.dirSync(); - fs.mkdirSync(path.join(projectDir, 'swr'), { recursive: true }); - fs.writeFileSync(path.join(projectDir, 'swr', 'test.txt'), 'hello'); - - await loadSchema( - ` - plugin swr { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/swr' - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - password String @omit - } - `, - { - pushDb: false, - projectDir, - extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`], - } - ); - - expect(fs.existsSync(path.join(projectDir, 'swr', 'test.txt'))).toBeFalsy(); - }); - - it('existing output as file', async () => { - const { name: projectDir } = tmp.dirSync(); - fs.writeFileSync(path.join(projectDir, 'swr'), 'hello'); - - await expect( - loadSchema( - ` - plugin swr { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/swr' - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String - password String @omit - } - `, - { pushDb: false, projectDir, extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`] } - ) - ).rejects.toThrow('already exists and is not a directory'); - }); -}); diff --git a/packages/plugins/swr/tests/test-model-meta.ts b/packages/plugins/swr/tests/test-model-meta.ts deleted file mode 100644 index 001d773a9..000000000 --- a/packages/plugins/swr/tests/test-model-meta.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ModelMeta } from '@zenstackhq/runtime/cross'; - -const fieldDefaults = { - isId: false, - isDataModel: false, - isArray: false, - isOptional: true, - attributes: [], - isRelationOwner: false, - isForeignKey: false, -}; - -export const modelMeta: ModelMeta = { - models: { - user: { - name: 'user', - fields: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, - }, - name: { ...fieldDefaults, type: 'String', name: 'name' }, - email: { ...fieldDefaults, type: 'String', name: 'name', isOptional: false }, - posts: { - ...fieldDefaults, - type: 'Post', - isDataModel: true, - isArray: true, - name: 'posts', - }, - }, - uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, - }, - post: { - name: 'post', - fields: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, - }, - title: { ...fieldDefaults, type: 'String', name: 'title' }, - owner: { ...fieldDefaults, type: 'User', name: 'owner', isDataModel: true, isRelationOwner: true }, - ownerId: { ...fieldDefaults, type: 'User', name: 'owner', isForeignKey: true }, - }, - uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, - }, - }, - deleteCascade: { - user: ['Post'], - }, -}; diff --git a/packages/plugins/swr/tsconfig.json b/packages/plugins/swr/tsconfig.json deleted file mode 100644 index 9e4f772c5..000000000 --- a/packages/plugins/swr/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "lib": ["ESNext", "DOM"], - "outDir": "dist", - "jsx": "react" - }, - "include": ["src/**/*.ts"], - "exclude": ["src/runtime"] -} diff --git a/packages/plugins/swr/tsup.config.ts b/packages/plugins/swr/tsup.config.ts deleted file mode 100644 index 2735372fa..000000000 --- a/packages/plugins/swr/tsup.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/runtime/index.ts'], - outDir: 'dist/runtime', - splitting: false, - sourcemap: true, - clean: true, - dts: true, - format: ['cjs', 'esm'], -}); diff --git a/packages/plugins/tanstack-query/CHANGELOG.md b/packages/plugins/tanstack-query/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/plugins/tanstack-query/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/plugins/tanstack-query/LICENSE b/packages/plugins/tanstack-query/LICENSE deleted file mode 120000 index 5853aaea5..000000000 --- a/packages/plugins/tanstack-query/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../../LICENSE \ No newline at end of file diff --git a/packages/plugins/tanstack-query/README.md b/packages/plugins/tanstack-query/README.md deleted file mode 100644 index 435ec13fd..000000000 --- a/packages/plugins/tanstack-query/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ZenStack tanstack-query plugin & runtime - -This package contains ZenStack plugin for generating [tanstack-query](https://tanstack.com/query/latest) hooks. - -Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/plugins/tanstack-query/jest.config.ts b/packages/plugins/tanstack-query/jest.config.ts deleted file mode 120000 index 09c104090..000000000 --- a/packages/plugins/tanstack-query/jest.config.ts +++ /dev/null @@ -1 +0,0 @@ -../../../jest.config.ts \ No newline at end of file diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json deleted file mode 100644 index 1bf1c871a..000000000 --- a/packages/plugins/tanstack-query/package.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "name": "@zenstackhq/tanstack-query", - "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "2.22.2", - "description": "ZenStack plugin for generating tanstack-query hooks", - "main": "index.js", - "exports": { - ".": { - "default": "./index.js" - }, - "./package.json": { - "default": "./package.json" - }, - "./runtime": { - "types": "./runtime/index.d.ts", - "import": "./runtime/index.mjs", - "require": "./runtime/index.js", - "default": "./runtime/index.js" - }, - "./runtime/react": { - "types": "./runtime/react.d.ts", - "import": "./runtime/react.mjs", - "require": "./runtime/react.js", - "default": "./runtime/react.js" - }, - "./runtime/vue": { - "types": "./runtime/vue.d.ts", - "import": "./runtime/vue.mjs", - "require": "./runtime/vue.js", - "default": "./runtime/vue.js" - }, - "./runtime/svelte": { - "types": "./runtime/svelte.d.ts", - "import": "./runtime/svelte.mjs", - "require": "./runtime/svelte.js", - "default": "./runtime/svelte.js" - }, - "./runtime-v5": { - "types": "./runtime-v5/index.d.ts", - "import": "./runtime-v5/index.mjs", - "require": "./runtime-v5/index.js", - "default": "./runtime-v5/index.js" - }, - "./runtime-v5/react": { - "types": "./runtime-v5/react.d.ts", - "import": "./runtime-v5/react.mjs", - "require": "./runtime-v5/react.js", - "default": "./runtime-v5/react.js" - }, - "./runtime-v5/vue": { - "types": "./runtime-v5/vue.d.ts", - "import": "./runtime-v5/vue.mjs", - "require": "./runtime-v5/vue.js", - "default": "./runtime-v5/vue.js" - }, - "./runtime-v5/svelte": { - "types": "./runtime-v5/svelte.d.ts", - "import": "./runtime-v5/svelte.mjs", - "require": "./runtime-v5/svelte.js", - "default": "./runtime-v5/svelte.js" - }, - "./runtime-v5/angular": { - "types": "./runtime-v5/angular.d.ts", - "import": "./runtime-v5/angular.mjs", - "require": "./runtime-v5/angular.js", - "default": "./runtime-v5/angular.js" - } - }, - "repository": { - "type": "git", - "url": "https://github.com/zenstackhq/zenstack" - }, - "scripts": { - "clean": "rimraf dist", - "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && tsup-node --config ./tsup-v5.config.ts && node scripts/postbuild && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination ../../../../.build", - "watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\" \"tsup-node --config ./tsup-v5.config.ts --watch\"", - "lint": "eslint src --ext ts", - "test": "jest", - "prepublishOnly": "pnpm build" - }, - "publishConfig": { - "directory": "dist", - "linkDirectory": true - }, - "keywords": [], - "author": "ZenStack Team", - "license": "MIT", - "dependencies": { - "@zenstackhq/runtime": "workspace:*", - "@zenstackhq/sdk": "workspace:*", - "cross-fetch": "^4.0.0", - "semver": "^7.5.2", - "ts-morph": "catalog:", - "ts-pattern": "^4.3.0" - }, - "devDependencies": { - "@angular/core": "^20.0.0", - "@angular/common": "^20.0.0", - "@angular/platform-browser": "^20.0.0", - "@tanstack/angular-query-v5": "npm:@tanstack/angular-query-experimental@5.84.x", - "@tanstack/react-query": "^4.29.7", - "@tanstack/react-query-v5": "npm:@tanstack/react-query@5.56.x", - "@tanstack/svelte-query": "^4.29.7", - "@tanstack/svelte-query-v5": "npm:@tanstack/svelte-query@^5.0.0", - "@tanstack/vue-query": "^4.37.0", - "@testing-library/react": "14.0.0", - "@types/react": "18.2.0", - "@types/semver": "^7.3.13", - "@types/tmp": "^0.2.3", - "@zenstackhq/testtools": "workspace:*", - "glob": "^8.1.0", - "jest-environment-jsdom": "^29.7.0", - "nock": "^13.3.6", - "react": "18.2.0", - "react-test-renderer": "^18.2.0", - "rxjs": "^7.8.0", - "svelte": "^4.2.1", - "swr": "^2.0.3", - "tmp": "^0.2.3", - "typescript": "^5.0.0", - "vue": "^3.3.4", - "zone.js": "^0.15.0" - } -} diff --git a/packages/plugins/tanstack-query/scripts/postbuild.js b/packages/plugins/tanstack-query/scripts/postbuild.js deleted file mode 100755 index 02abe2dfb..000000000 --- a/packages/plugins/tanstack-query/scripts/postbuild.js +++ /dev/null @@ -1,54 +0,0 @@ -// tsup doesn't replace npm dependency aliases in the dist files, so we have to do it manually - -const fs = require('fs'); -const glob = require('glob'); - -function replaceSync({ file, from, to }) { - const paths = glob.sync(file, { ignore: [], nodir: true }); - - paths.forEach(path => { - const contents = fs.readFileSync(path, { encoding: 'utf-8' }); - - const newContents = contents.replace(from, to); - - if (newContents !== contents) { - fs.writeFileSync(path, newContents, { encoding: 'utf-8' }); - } - }); -} - -// tsup incorrectly resolve to legacy types, make a fix here -console.log('Replacing @tanstack/react-query-v5/build/legacy/types'); -replaceSync({ - file: 'dist/runtime-v5/react*(.d.ts|.d.mts)', - from: /@tanstack\/react-query-v5\/build\/legacy\/types/g, - to: '@tanstack/react-query', -}); - -console.log('Replacing @tanstack/react-query-v5'); -replaceSync({ - file: 'dist/runtime-v5/react*(.d.ts|.d.mts|.js|.mjs)', - from: /@tanstack\/react-query-v5/g, - to: '@tanstack/react-query', -}); - -console.log('Replacing @tanstack/svelte-query-v5'); -replaceSync({ - file: 'dist/runtime-v5/svelte*(.d.ts|.d.mts|.js|.mjs)', - from: /@tanstack\/svelte-query-v5/g, - to: '@tanstack/svelte-query', -}); - -console.log('Replacing @tanstack/vue-query-v5'); -replaceSync({ - file: 'dist/runtime-v5/vue*(.d.ts|.d.mts|.js|.mjs)', - from: /@tanstack\/vue-query-v5/g, - to: '@tanstack/vue-query', -}); - -console.log('Replacing @tanstack/angular-query-v5'); -replaceSync({ - file: 'dist/runtime-v5/angular*(.d.ts|.d.mts|.js|.mjs)', - from: /@tanstack\/angular-query-v5/g, - to: '@tanstack/angular-query-experimental', -}); diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts deleted file mode 100644 index cc3bc22cf..000000000 --- a/packages/plugins/tanstack-query/src/generator.ts +++ /dev/null @@ -1,788 +0,0 @@ -import { - PluginError, - PluginOptions, - RUNTIME_PACKAGE, - createProject, - ensureEmptyDir, - generateModelMeta, - getDataModels, - getPrismaClientGenerator, - isDelegateModel, - requireOption, - resolvePath, - saveProject, -} from '@zenstackhq/sdk'; -import { DataModel, DataModelFieldType, Model, isEnum, isTypeDef } from '@zenstackhq/sdk/ast'; -import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; -import { lowerCaseFirst, upperCaseFirst, paramCase } from '@zenstackhq/runtime/local-helpers'; -import fs from 'fs'; -import path from 'path'; -import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; -import { P, match } from 'ts-pattern'; -import { name } from '.'; - -const supportedTargets = ['react', 'vue', 'svelte', 'angular']; -type TargetFramework = (typeof supportedTargets)[number]; -type TanStackVersion = 'v4' | 'v5'; - -export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - const project = createProject(); - const warnings: string[] = []; - const models = getDataModels(model); - const typeDefs = model.declarations.filter(isTypeDef); - - const target = requireOption(options, 'target', name); - if (!supportedTargets.includes(target)) { - throw new PluginError(name, `Unsupported target "${target}", supported values: ${supportedTargets.join(', ')}`); - } - - const version = typeof options.version === 'string' ? options.version : 'v5'; - if (version !== 'v4' && version !== 'v5') { - throw new PluginError(name, `Unsupported version "${version}": use "v4" or "v5"`); - } - - // Angular is only supported in v5 - if (target === 'angular' && version !== 'v5') { - throw new PluginError(name, `Angular target is only supported with version "v5", got "${version}"`); - } - - let outDir = requireOption(options, 'output', name); - outDir = resolvePath(outDir, options); - ensureEmptyDir(outDir); - - if (options.portable && typeof options.portable !== 'boolean') { - throw new PluginError( - name, - `Invalid value for "portable" option: ${options.portable}, a boolean value is expected` - ); - } - - await generateModelMeta(project, models, typeDefs, { - output: path.join(outDir, '__model_meta.ts'), - generateAttributes: false, - }); - - generateIndex(project, outDir, models, target, version); - - models.forEach((dataModel) => { - const mapping = dmmf.mappings.modelOperations.find((op) => op.model === dataModel.name); - if (!mapping) { - warnings.push(`Unable to find mapping for model ${dataModel.name}`); - return; - } - generateModelHooks(target, version, project, outDir, dataModel, mapping, options); - }); - - if (options.portable) { - const gen = getPrismaClientGenerator(model); - if (gen?.isNewGenerator) { - warnings.push(`The "portable" option is not supported with the "prisma-client" generator and is ignored.`); - } else { - generateBundledTypes(project, outDir, options); - } - } - - await saveProject(project); - return { warnings }; -} - -function generateQueryHook( - target: TargetFramework, - version: TanStackVersion, - sf: SourceFile, - model: string, - operation: string, - returnArray: boolean, - optionalInput: boolean, - overrideReturnType?: string, - overrideInputType?: string, - overrideTypeParameters?: string[], - supportInfinite = false, - supportOptimistic = false -) { - const generateModes: ('' | 'Infinite' | 'Suspense' | 'SuspenseInfinite')[] = ['']; - if (supportInfinite) { - generateModes.push('Infinite'); - } - - if (target === 'react' && version === 'v5') { - // react-query v5 supports suspense query - generateModes.push('Suspense'); - if (supportInfinite) { - generateModes.push('SuspenseInfinite'); - } - } - - for (const generateMode of generateModes) { - const capOperation = upperCaseFirst(operation); - - const argsType = overrideInputType ?? `Prisma.${model}${capOperation}Args`; - let inputType = makeQueryArgsType(target, argsType); - if (target === 'angular') { - inputType = `${inputType} | (() => ${inputType})`; - } - - const infinite = generateMode.includes('Infinite'); - const suspense = generateMode.includes('Suspense'); - const optimistic = - supportOptimistic && - // infinite queries are not subject to optimistic updates - !infinite; - - let defaultReturnType = `Prisma.${model}GetPayload`; - if (optimistic) { - defaultReturnType += '& { $optimistic?: boolean }'; - } - if (returnArray) { - defaultReturnType = `Array<${defaultReturnType}>`; - } - - const returnType = overrideReturnType ?? defaultReturnType; - const optionsType = makeQueryOptions(target, 'TQueryFnData', 'TData', infinite, suspense, version); - - const func = sf.addFunction({ - name: `use${generateMode}${capOperation}${model}`, - typeParameters: overrideTypeParameters ?? [ - `TArgs extends ${argsType}`, - `TQueryFnData = ${returnType} `, - 'TData = TQueryFnData', - 'TError = DefaultError', - ], - parameters: [ - { - name: optionalInput ? 'args?' : 'args', - type: inputType, - }, - { - name: 'options?', - type: optionsType, - }, - ], - isExported: true, - }); - - if (version === 'v5' && infinite && ['react', 'svelte', 'angular'].includes(target)) { - // getNextPageParam option is required in v5 - func.addStatements([`options = options ?? { getNextPageParam: () => null };`]); - } - - func.addStatements([ - makeGetContext(target), - `return use${generateMode}ModelQuery('${model}', \`\${endpoint}/${lowerCaseFirst( - model - )}/${operation}\`, args, options, fetch);`, - ]); - } -} - -function generateMutationHook( - target: TargetFramework, - sf: SourceFile, - model: string, - operation: string, - httpVerb: 'post' | 'put' | 'delete', - checkReadBack: boolean, - overrideReturnType?: string -) { - const capOperation = upperCaseFirst(operation); - - const argsType = `Prisma.${model}${capOperation}Args`; - const inputType = `Prisma.SelectSubset`; - let returnType = overrideReturnType ?? `CheckSelect>`; - if (checkReadBack) { - returnType = `(${returnType} | undefined )`; - } - const nonGenericOptionsType = `Omit<${makeMutationOptions( - target, - checkReadBack ? `(${overrideReturnType ?? model} | undefined)` : overrideReturnType ?? model, - argsType - )}, 'mutationFn'>`; - const optionsType = `Omit<${makeMutationOptions(target, returnType, inputType)}, 'mutationFn'>`; - - const func = sf.addFunction({ - name: `use${capOperation}${model}`, - isExported: true, - parameters: [ - { - name: 'options?', - type: nonGenericOptionsType, - }, - ], - }); - - // get endpoint from context - func.addStatements([makeGetContext(target)]); - - func.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: `_mutation`, - initializer: ` - useModelMutation<${argsType}, DefaultError, ${ - overrideReturnType ?? model - }, ${checkReadBack}>('${model}', '${httpVerb.toUpperCase()}', \`\${endpoint}/${lowerCaseFirst( - model - )}/${operation}\`, metadata, options, fetch, ${checkReadBack}) - `, - }, - ], - }); - - switch (target) { - case 'react': - case 'vue': - case 'angular': - // override the mutateAsync function to return the correct type - func.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: 'mutation', - initializer: `{ - ..._mutation, - mutateAsync: async ( - args: Prisma.SelectSubset, - options?: ${optionsType} - ) => { - return (await _mutation.mutateAsync( - args, - options as any - )) as ${returnType}; - }, - }`, - }, - ], - }); - break; - - case 'svelte': - // svelte-query returns a store for mutations - // here we override the mutateAsync function to return the correct type - // and call `derived` to return a new reactive store - func.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: 'mutation', - initializer: `derived(_mutation, value => ({ - ...value, - mutateAsync: async ( - args: Prisma.SelectSubset, - options?: ${optionsType} - ) => { - return (await value.mutateAsync( - args, - options as any - )) as ${returnType}; - }, - }))`, - }, - ], - }); - break; - - default: - throw new PluginError(name, `Unsupported target "${target}"`); - } - - func.addStatements('return mutation;'); -} - -function generateCheckHook( - target: string, - version: TanStackVersion, - sf: SourceFile, - model: DataModel, - prismaImport: string -) { - const mapFilterType = (type: DataModelFieldType) => { - return match(type.type) - .with(P.union('Int', 'BigInt'), () => 'number') - .with('String', () => 'string') - .with('Boolean', () => 'boolean') - .otherwise(() => undefined); - }; - - const filterFields: Array<{ name: string; type: string }> = []; - const enumsToImport = new Set(); - - // collect filterable fields and enums to import - model.fields.forEach((f) => { - if (isEnum(f.type.reference?.ref)) { - enumsToImport.add(f.type.reference.$refText); - filterFields.push({ name: f.name, type: f.type.reference.$refText }); - } - - const mappedType = mapFilterType(f.type); - if (mappedType) { - filterFields.push({ name: f.name, type: mappedType }); - } - }); - - if (enumsToImport.size > 0) { - // import enums - sf.addStatements(`import type { ${Array.from(enumsToImport).join(', ')} } from '${prismaImport}';`); - } - - const whereType = `{ ${filterFields.map(({ name, type }) => `${name}?: ${type}`).join('; ')} }`; - - const func = sf.addFunction({ - name: `useCheck${model.name}`, - isExported: true, - typeParameters: ['TError = DefaultError'], - parameters: [ - { name: 'args', type: `{ operation: PolicyCrudKind; where?: ${whereType}; }` }, - { name: 'options?', type: makeQueryOptions(target, 'boolean', 'boolean', false, false, version) }, - ], - }); - - func.addStatements([ - makeGetContext(target), - `return useModelQuery('${model.name}', \`\${endpoint}/${lowerCaseFirst( - model.name - )}/check\`, args, options, fetch);`, - ]); -} - -function generateModelHooks( - target: TargetFramework, - version: TanStackVersion, - project: Project, - outDir: string, - model: DataModel, - mapping: DMMF.ModelMapping, - options: PluginOptions -) { - const modelNameCap = upperCaseFirst(model.name); - const fileName = paramCase(model.name); - const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); - - const prismaImport = options.portable ? './__types' : getPrismaClientImportSpec(outDir, options); - sf.addImportDeclaration({ - namedImports: ['Prisma', model.name], - isTypeOnly: true, - moduleSpecifier: prismaImport, - }); - sf.addStatements(makeBaseImports(target, version)); - - // Note: delegate models don't support create and upsert operations - - // create is somehow named "createOne" in the DMMF - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!isDelegateModel(model) && (mapping.create || (mapping as any).createOne)) { - generateMutationHook(target, sf, model.name, 'create', 'post', true); - } - - // createMany - if (!isDelegateModel(model) && mapping.createMany && supportCreateMany(model.$container)) { - generateMutationHook(target, sf, model.name, 'createMany', 'post', false, 'Prisma.BatchPayload'); - } - - // findMany - if (mapping.findMany) { - // regular findMany - generateQueryHook( - target, - version, - sf, - model.name, - 'findMany', - true, - true, - undefined, - undefined, - undefined, - true, - true - ); - } - - // findUnique - if (mapping.findUnique) { - generateQueryHook( - target, - version, - sf, - model.name, - 'findUnique', - false, - false, - undefined, - undefined, - undefined, - false, - true - ); - } - - // findFirst - if (mapping.findFirst) { - generateQueryHook( - target, - version, - sf, - model.name, - 'findFirst', - false, - true, - undefined, - undefined, - undefined, - false, - true - ); - } - - // update - // update is somehow named "updateOne" in the DMMF - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (mapping.update || (mapping as any).updateOne) { - generateMutationHook(target, sf, model.name, 'update', 'put', true); - } - - // updateMany - if (mapping.updateMany) { - generateMutationHook(target, sf, model.name, 'updateMany', 'put', false, 'Prisma.BatchPayload'); - } - - // upsert - // upsert is somehow named "upsertOne" in the DMMF - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!isDelegateModel(model) && (mapping.upsert || (mapping as any).upsertOne)) { - generateMutationHook(target, sf, model.name, 'upsert', 'post', true); - } - - // del - // delete is somehow named "deleteOne" in the DMMF - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (mapping.delete || (mapping as any).deleteOne) { - generateMutationHook(target, sf, model.name, 'delete', 'delete', true); - } - - // deleteMany - if (mapping.deleteMany) { - generateMutationHook(target, sf, model.name, 'deleteMany', 'delete', false, 'Prisma.BatchPayload'); - } - - // aggregate - if (mapping.aggregate) { - generateQueryHook( - target, - version, - sf, - modelNameCap, - 'aggregate', - false, - false, - `Prisma.Get${modelNameCap}AggregateType` - ); - } - - // groupBy - if (mapping.groupBy) { - const useName = model.name; - - const returnType = `{} extends InputErrors ? - Array & - { - [P in ((keyof TArgs) & (keyof Prisma.${modelNameCap}GroupByOutputType))]: P extends '_count' - ? TArgs[P] extends boolean - ? number - : Prisma.GetScalarType - : Prisma.GetScalarType - } - > : InputErrors`; - - const typeParameters = [ - `TArgs extends Prisma.${useName}GroupByArgs`, - `HasSelectOrTake extends Prisma.Or>, Prisma.Extends<'take', Prisma.Keys>>`, - `OrderByArg extends Prisma.True extends HasSelectOrTake ? { orderBy: Prisma.${useName}GroupByArgs['orderBy'] }: { orderBy?: Prisma.${useName}GroupByArgs['orderBy'] },`, - `OrderFields extends Prisma.ExcludeUnderscoreKeys>>`, - `ByFields extends Prisma.MaybeTupleToUnion`, - `ByValid extends Prisma.Has`, - `HavingFields extends Prisma.GetHavingFields`, - `HavingValid extends Prisma.Has`, - `ByEmpty extends TArgs['by'] extends never[] ? Prisma.True : Prisma.False`, - `InputErrors extends ByEmpty extends Prisma.True - ? \`Error: "by" must not be empty.\` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? \`Error: Field "\${P}" used in "having" needs to be provided in "by".\` - : [ - Error, - 'Field ', - P, - \` in "having" needs to be provided in "by"\`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields]`, - `TQueryFnData = ${returnType}`, - `TData = TQueryFnData`, - `TError = DefaultError`, - ]; - - generateQueryHook( - target, - version, - sf, - model.name, - 'groupBy', - false, - false, - returnType, - `Prisma.SubsetIntersection & InputErrors`, - typeParameters - ); - } - - // somehow dmmf doesn't contain "count" operation, so we unconditionally add it here - { - generateQueryHook( - target, - version, - sf, - model.name, - 'count', - false, - true, - `TArgs extends { select: any; } ? TArgs['select'] extends true ? number : Prisma.GetScalarType : number` - ); - } - - { - // extra `check` hook for ZenStack's permission checker API - generateCheckHook(target, version, sf, model, prismaImport); - } -} - -function generateIndex( - project: Project, - outDir: string, - models: DataModel[], - target: string, - version: TanStackVersion -) { - const runtimeImportBase = makeRuntimeImportBase(version); - const sf = project.createSourceFile(path.join(outDir, 'index.ts'), undefined, { overwrite: true }); - sf.addStatements(models.map((d) => `export * from './${paramCase(d.name)}';`)); - sf.addStatements(`export { getQueryKey } from '${runtimeImportBase}';`); - switch (target) { - case 'react': - sf.addStatements(`export { Provider } from '${runtimeImportBase}/react';`); - break; - case 'vue': - sf.addStatements(`export { VueQueryContextKey, provideHooksContext } from '${runtimeImportBase}/vue';`); - break; - case 'svelte': - sf.addStatements(`export { SvelteQueryContextKey, setHooksContext } from '${runtimeImportBase}/svelte';`); - break; - case 'angular': - sf.addStatements( - `export { AngularQueryContextKey, provideAngularQueryContext } from '${runtimeImportBase}/angular';` - ); - break; - } - sf.addStatements(`export { default as metadata } from './__model_meta';`); -} - -function makeGetContext(target: TargetFramework) { - switch (target) { - case 'react': - return 'const { endpoint, fetch } = getHooksContext();'; - case 'vue': - return 'const { endpoint, fetch } = getHooksContext();'; - case 'svelte': - return `const { endpoint, fetch } = getHooksContext();`; - case 'angular': - return 'const { endpoint, fetch } = getHooksContext();'; - default: - throw new PluginError(name, `Unsupported target "${target}"`); - } -} - -function makeBaseImports(target: TargetFramework, version: TanStackVersion) { - const runtimeImportBase = makeRuntimeImportBase(version); - const shared = [ - `import { useModelQuery, useInfiniteModelQuery, useModelMutation } from '${runtimeImportBase}/${target}';`, - `import type { PickEnumerable, CheckSelect, QueryError, ExtraQueryOptions, ExtraMutationOptions } from '${runtimeImportBase}';`, - `import type { PolicyCrudKind } from '${RUNTIME_PACKAGE}'`, - `import metadata from './__model_meta';`, - `type DefaultError = QueryError;`, - ]; - switch (target) { - case 'react': { - const suspense = - version === 'v5' - ? [ - `import { useSuspenseModelQuery, useSuspenseInfiniteModelQuery } from '${runtimeImportBase}/${target}';`, - `import type { UseSuspenseQueryOptions, UseSuspenseInfiniteQueryOptions } from '@tanstack/react-query';`, - ] - : []; - return [ - `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query';`, - `import { getHooksContext } from '${runtimeImportBase}/${target}';`, - ...shared, - ...suspense, - ]; - } - case 'vue': { - return [ - `import type { UseMutationOptions, UseQueryOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/vue-query';`, - `import { getHooksContext } from '${runtimeImportBase}/${target}';`, - `import type { MaybeRefOrGetter, ComputedRef, UnwrapRef } from 'vue';`, - ...shared, - ]; - } - case 'svelte': { - return [ - `import { derived } from 'svelte/store';`, - `import type { MutationOptions, CreateQueryOptions, CreateInfiniteQueryOptions } from '@tanstack/svelte-query';`, - ...(version === 'v5' - ? [`import type { InfiniteData, StoreOrVal } from '@tanstack/svelte-query';`] - : []), - `import { getHooksContext } from '${runtimeImportBase}/${target}';`, - ...shared, - ]; - } - case 'angular': { - return [ - `import type { CreateMutationOptions, CreateQueryOptions, CreateInfiniteQueryOptions, InfiniteData } from '@tanstack/angular-query-experimental';`, - `import { getHooksContext } from '${runtimeImportBase}/${target}';`, - ...shared, - ]; - } - default: - throw new PluginError(name, `Unsupported target: ${target}`); - } -} - -function makeQueryArgsType(target: string, argsType: string) { - const type = `Prisma.SelectSubset`; - if (target === 'vue') { - return `MaybeRefOrGetter<${type}> | ComputedRef<${type}>`; - } else { - return type; - } -} - -function makeQueryOptions( - target: string, - returnType: string, - dataType: string, - infinite: boolean, - suspense: boolean, - version: TanStackVersion -) { - let result = match(target) - .with('react', () => - infinite - ? version === 'v4' - ? `Omit, 'queryKey'>` - : `Omit>, 'queryKey' | 'initialPageParam'>` - : `Omit, 'queryKey'>` - ) - .with('vue', () => { - const baseOption = infinite - ? `Omit>, 'queryKey' | 'initialPageParam'>` - : version === 'v4' - ? `Omit, 'queryKey'>` - : `Omit>, 'queryKey'>`; - return `MaybeRefOrGetter<${baseOption}> | ComputedRef<${baseOption}>`; - }) - .with('svelte', () => - infinite - ? version === 'v4' - ? `Omit, 'queryKey'>` - : `StoreOrVal>, 'queryKey' | 'initialPageParam'>>` - : version === 'v4' - ? `Omit, 'queryKey'>` - : `StoreOrVal, 'queryKey'>>` - ) - .with('angular', () => - infinite - ? `Omit>, 'queryKey' | 'initialPageParam'>` - : `Omit, 'queryKey'>` - ) - .otherwise(() => { - throw new PluginError(name, `Unsupported target: ${target}`); - }); - - if (!infinite) { - // non-infinite queries support extra options like optimistic updates - result = `(${result} & ExtraQueryOptions)`; - } - - return result; -} - -function makeMutationOptions(target: string, returnType: string, argsType: string) { - let result = match(target) - .with('react', () => `UseMutationOptions<${returnType}, DefaultError, ${argsType}>`) - .with('vue', () => { - const baseOption = `UseMutationOptions<${returnType}, DefaultError, ${argsType}, unknown>`; - return `MaybeRefOrGetter<${baseOption}> | ComputedRef<${baseOption}>`; - }) - .with('svelte', () => `MutationOptions<${returnType}, DefaultError, ${argsType}>`) - .with('angular', () => `CreateMutationOptions<${returnType}, DefaultError, ${argsType}>`) - .otherwise(() => { - throw new PluginError(name, `Unsupported target: ${target}`); - }); - - result = `(${result} & ExtraMutationOptions)`; - - return result; -} - -function makeRuntimeImportBase(version: TanStackVersion) { - return `@zenstackhq/tanstack-query/runtime${version === 'v5' ? '-v5' : ''}`; -} - -function generateBundledTypes(project: Project, outDir: string, options: PluginOptions) { - if (!options.prismaClientDtsPath) { - throw new PluginError(name, `Unable to determine the location of PrismaClient types`); - } - - // copy PrismaClient index.d.ts - const content = fs.readFileSync(options.prismaClientDtsPath, 'utf-8'); - project.createSourceFile(path.join(outDir, '__types.d.ts'), content, { overwrite: true }); - - // "runtime/library.d.ts" is referenced by Prisma's DTS, and it's generated into Prisma's output - // folder if a custom output is specified; if not, it's referenced from '@prisma/client' - const libraryDts = path.join(path.dirname(options.prismaClientDtsPath), 'runtime', 'library.d.ts'); - if (fs.existsSync(libraryDts)) { - const content = fs.readFileSync(libraryDts, 'utf-8'); - project.createSourceFile(path.join(outDir, 'runtime', 'library.d.ts'), content, { overwrite: true }); - } -} diff --git a/packages/plugins/tanstack-query/src/index.ts b/packages/plugins/tanstack-query/src/index.ts deleted file mode 100644 index eb315e00c..000000000 --- a/packages/plugins/tanstack-query/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { PluginFunction } from '@zenstackhq/sdk'; -import { generate } from './generator'; - -export const name = 'Tanstack Query'; - -const run: PluginFunction = async (model, options, dmmf) => { - if (!dmmf) { - throw new Error('DMMF is required'); - } - return generate(model, options, dmmf); -}; - -export default run; diff --git a/packages/plugins/tanstack-query/src/runtime-v5/angular.ts b/packages/plugins/tanstack-query/src/runtime-v5/angular.ts deleted file mode 100644 index c5a67c44f..000000000 --- a/packages/plugins/tanstack-query/src/runtime-v5/angular.ts +++ /dev/null @@ -1,213 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - injectQuery, - injectMutation, - injectInfiniteQuery, - QueryClient, - type CreateQueryOptions, - type CreateMutationOptions, - type CreateInfiniteQueryOptions, - type InfiniteData, -} from '@tanstack/angular-query-v5'; -import type { ModelMeta } from '@zenstackhq/runtime/cross'; -import { inject, InjectionToken } from '@angular/core'; -import { - APIContext, - DEFAULT_QUERY_ENDPOINT, - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, -} from '../runtime/common'; - -export { APIContext as RequestHandlerContext } from '../runtime/common'; - -type AnyFn = (...a: unknown[]) => unknown; -const isFn = (v: unknown): v is AnyFn => typeof v === 'function'; - -export const AngularQueryContextKey = new InjectionToken('zenstack-angular-query-context'); - -/** - * Provide context for the generated TanStack Query hooks. - */ -export function provideAngularQueryContext(context: APIContext) { - return { - provide: AngularQueryContextKey, - useValue: context, - }; -} - -/** - * Hooks context. - */ -export function getHooksContext() { - const context = inject(AngularQueryContextKey, { - optional: true, - }) || { - endpoint: DEFAULT_QUERY_ENDPOINT, - fetch: undefined, - logging: false, - }; - - const { endpoint, ...rest } = context; - return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; -} - -/** - * Creates an Angular TanStack Query query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The request args object, URL-encoded and appended as "?q=" parameter - * @param options The Angular query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns injectQuery hook - */ -export function useModelQuery( - model: string, - url: string, - args?: unknown, - options?: Omit, 'queryKey'> & ExtraQueryOptions, - fetch?: FetchFn -) { - const query = injectQuery(() => { - const resolvedArgs = isFn(args) ? args() : args; - - const reqUrl = makeUrl(url, resolvedArgs); - const queryKey = getQueryKey(model, url, resolvedArgs, { - infinite: false, - optimisticUpdate: options?.optimisticUpdate !== false, - }); - return { - queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), - ...options, - }; - }); - return query; -} - -/** - * Creates an Angular TanStack Query infinite query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The initial request args object, URL-encoded and appended as "?q=" parameter - * @param options The Angular infinite query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns injectInfiniteQuery hook - */ -export function useInfiniteModelQuery( - model: string, - url: string, - args: unknown, - options: Omit< - CreateInfiniteQueryOptions>, - 'queryKey' | 'initialPageParam' - >, - fetch?: FetchFn -) { - const query = injectInfiniteQuery(() => { - const resolvedArgs = isFn(args) ? args() : args; - - const queryKey = getQueryKey(model, url, resolvedArgs, { infinite: true, optimisticUpdate: false }); - - return { - queryKey, - queryFn: ({ pageParam, signal }) => { - return fetcher(makeUrl(url, pageParam ?? resolvedArgs), { signal }, fetch, false); - }, - initialPageParam: resolvedArgs, - ...options, - }; - }); - return query; -} - -/** - * Creates an Angular TanStack Query mutation. - * - * @param model The name of the model under mutation. - * @param method The HTTP method. - * @param url The request URL. - * @param modelMeta The model metadata. - * @param options The Angular mutation options. - * @param fetch The fetch function to use for sending the HTTP request - * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @returns injectMutation hook - */ -export function useModelMutation< - TArgs, - TError, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R ->( - model: string, - method: 'POST' | 'PUT' | 'DELETE', - url: string, - modelMeta: ModelMeta, - options?: Omit, 'mutationFn'> & ExtraMutationOptions, - fetch?: FetchFn, - checkReadBack?: C -) { - const queryClient = inject(QueryClient); - const mutationFn = (data: unknown) => { - const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; - const fetchInit: RequestInit = { - method, - ...(method !== 'DELETE' && { - headers: { - 'content-type': 'application/json', - }, - body: marshal(data), - }), - }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; - }; - - const finalOptions = { ...options, mutationFn }; - const operation = url.split('/').pop(); - const invalidateQueries = options?.invalidateQueries !== false; - const optimisticUpdate = !!options?.optimisticUpdate; - - if (operation) { - const { logging } = getHooksContext(); - if (invalidateQueries) { - setupInvalidation( - model, - operation, - modelMeta, - finalOptions, - (predicate) => queryClient.invalidateQueries({ predicate }), - logging - ); - } - - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - modelMeta, - finalOptions, - queryClient.getQueryCache().getAll(), - (queryKey, data) => { - // update query cache - queryClient.setQueryData(queryKey, data); - // cancel on-flight queries to avoid redundant cache updates, - // the settlement of the current mutation will trigger a new revalidation - queryClient.cancelQueries({ queryKey }, { revert: false, silent: true }); - }, - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging - ); - } - } - - return injectMutation(() => finalOptions); -} diff --git a/packages/plugins/tanstack-query/src/runtime-v5/index.ts b/packages/plugins/tanstack-query/src/runtime-v5/index.ts deleted file mode 100644 index ee494ca7d..000000000 --- a/packages/plugins/tanstack-query/src/runtime-v5/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - getQueryKey, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, - type QueryError, -} from '../runtime/common'; -export * from '../runtime/prisma-types'; diff --git a/packages/plugins/tanstack-query/src/runtime-v5/react.ts b/packages/plugins/tanstack-query/src/runtime-v5/react.ts deleted file mode 100644 index 4c8c5ceb6..000000000 --- a/packages/plugins/tanstack-query/src/runtime-v5/react.ts +++ /dev/null @@ -1,262 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - UseSuspenseInfiniteQueryOptions, - UseSuspenseQueryOptions, - useInfiniteQuery, - useMutation, - useQuery, - useQueryClient, - useSuspenseInfiniteQuery, - useSuspenseQuery, - type InfiniteData, - type UseInfiniteQueryOptions, - type UseMutationOptions, - type UseQueryOptions, -} from '@tanstack/react-query-v5'; -import type { ModelMeta } from '@zenstackhq/runtime/cross'; -import { createContext, useContext } from 'react'; -import { - DEFAULT_QUERY_ENDPOINT, - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type APIContext, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, -} from '../runtime/common'; - -/** - * Context for configuring react hooks. - */ -export const RequestHandlerContext = createContext({ - endpoint: DEFAULT_QUERY_ENDPOINT, - fetch: undefined, -}); - -/** - * Hooks context. - */ -export function getHooksContext() { - const { endpoint, ...rest } = useContext(RequestHandlerContext); - return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; -} - -/** - * Context provider. - */ -export const Provider = RequestHandlerContext.Provider; - -/** - * Creates a react-query query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The request args object, URL-encoded and appended as "?q=" parameter - * @param options The react-query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook - */ -export function useModelQuery( - model: string, - url: string, - args?: unknown, - options?: Omit, 'queryKey'> & ExtraQueryOptions, - fetch?: FetchFn -) { - const reqUrl = makeUrl(url, args); - const queryKey = getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: options?.optimisticUpdate !== false, - }); - return { - queryKey, - ...useQuery({ - queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), - ...options, - }), - }; -} - -/** - * Creates a react-query suspense query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The request args object, URL-encoded and appended as "?q=" parameter - * @param options The react-query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useSuspenseQuery hook - */ -export function useSuspenseModelQuery( - model: string, - url: string, - args?: unknown, - options?: Omit, 'queryKey'> & ExtraQueryOptions, - fetch?: FetchFn -) { - const reqUrl = makeUrl(url, args); - const queryKey = getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: options?.optimisticUpdate !== false, - }); - return { - queryKey, - ...useSuspenseQuery({ - queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), - ...options, - }), - }; -} - -/** - * Creates a react-query infinite query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The initial request args object, URL-encoded and appended as "?q=" parameter - * @param options The react-query infinite query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useInfiniteQuery hook - */ -export function useInfiniteModelQuery( - model: string, - url: string, - args: unknown, - options: Omit>, 'queryKey' | 'initialPageParam'>, - fetch?: FetchFn -) { - const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); - return { - queryKey, - ...useInfiniteQuery({ - queryKey, - queryFn: ({ pageParam, signal }) => { - return fetcher(makeUrl(url, pageParam ?? args), { signal }, fetch, false); - }, - initialPageParam: args, - ...options, - }), - }; -} - -/** - * Creates a react-query infinite suspense query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The initial request args object, URL-encoded and appended as "?q=" parameter - * @param options The react-query infinite query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useSuspenseInfiniteQuery hook - */ -export function useSuspenseInfiniteModelQuery( - model: string, - url: string, - args: unknown, - options: Omit< - UseSuspenseInfiniteQueryOptions>, - 'queryKey' | 'initialPageParam' - >, - fetch?: FetchFn -) { - const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); - return { - queryKey, - ...useSuspenseInfiniteQuery({ - queryKey, - queryFn: ({ pageParam, signal }) => { - return fetcher(makeUrl(url, pageParam ?? args), { signal }, fetch, false); - }, - initialPageParam: args, - ...options, - }), - }; -} - -/** - * Creates a react-query mutation - * - * @param model The name of the model under mutation. - * @param method The HTTP method. - * @param url The request URL. - * @param modelMeta The model metadata. - * @param options The react-query options. - * @param fetch The fetch function to use for sending the HTTP request - * @param checkReadBack Whether to check for read back errors and return undefined if found. - */ -export function useModelMutation< - TArgs, - TError, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R ->( - model: string, - method: 'POST' | 'PUT' | 'DELETE', - url: string, - modelMeta: ModelMeta, - options?: Omit, 'mutationFn'> & ExtraMutationOptions, - fetch?: FetchFn, - checkReadBack?: C -) { - const queryClient = useQueryClient(); - const mutationFn = (data: any) => { - const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; - const fetchInit: RequestInit = { - method, - ...(method !== 'DELETE' && { - headers: { - 'content-type': 'application/json', - }, - body: marshal(data), - }), - }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; - }; - - const finalOptions = { ...options, mutationFn }; - const operation = url.split('/').pop(); - const invalidateQueries = options?.invalidateQueries !== false; - const optimisticUpdate = !!options?.optimisticUpdate; - - if (operation) { - const { logging } = useContext(RequestHandlerContext); - if (invalidateQueries) { - setupInvalidation( - model, - operation, - modelMeta, - finalOptions, - (predicate) => queryClient.invalidateQueries({ predicate }), - logging - ); - } - - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - modelMeta, - finalOptions, - queryClient.getQueryCache().getAll(), - (queryKey, data) => { - // update query cache - queryClient.setQueryData(queryKey, data); - // cancel on-flight queries to avoid redundant cache updates, - // the settlement of the current mutation will trigger a new revalidation - queryClient.cancelQueries({ queryKey }, { revert: false, silent: true }); - }, - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging - ); - } - } - - return useMutation(finalOptions); -} diff --git a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts b/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts deleted file mode 100644 index c09ea89ef..000000000 --- a/packages/plugins/tanstack-query/src/runtime-v5/svelte.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { QueryKey } from '@tanstack/react-query-v5'; -import { - createInfiniteQuery, - createMutation, - createQuery, - useQueryClient, - type CreateInfiniteQueryOptions, - type CreateQueryOptions, - type InfiniteData, - type MutationOptions, - type QueryFunction, - type StoreOrVal, -} from '@tanstack/svelte-query-v5'; -import { ModelMeta } from '@zenstackhq/runtime/cross'; -import { getContext, setContext } from 'svelte'; -import { derived, Readable } from 'svelte/store'; -import { - APIContext, - DEFAULT_QUERY_ENDPOINT, - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, -} from '../runtime/common'; - -export { APIContext as RequestHandlerContext } from '../runtime/common'; - -/** - * Key for setting and getting the global query context. - */ -export const SvelteQueryContextKey = 'zenstack-svelte-query-context'; - -/** - * Set context for the generated TanStack Query hooks. - */ -export function setHooksContext(context: APIContext) { - setContext(SvelteQueryContextKey, context); -} - -/** - * Hooks context. - */ -export function getHooksContext() { - const { endpoint, ...rest } = getContext(SvelteQueryContextKey); - return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; -} - -/** - * Creates a svelte-query query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The request args object, URL-encoded and appended as "?q=" parameter - * @param options The svelte-query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook - */ -export function useModelQuery( - model: string, - url: string, - args?: unknown, - options?: StoreOrVal, 'queryKey'>> & ExtraQueryOptions, - fetch?: FetchFn -) { - const reqUrl = makeUrl(url, args); - const queryKey = getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: options?.optimisticUpdate !== false, - }); - const queryFn: QueryFunction = ({ signal }) => - fetcher(reqUrl, { signal }, fetch, false); - - let mergedOpt: any; - if (isStore(options)) { - // options is store - mergedOpt = derived([options], ([$opt]) => { - return { - queryKey, - queryFn, - ...($opt as object), - }; - }); - } else { - // options is value - mergedOpt = { - queryKey, - queryFn, - ...options, - }; - } - - const result = createQuery(mergedOpt); - return derived(result, (r) => ({ - queryKey, - ...r, - })); -} - -/** - * Creates a svelte-query infinite query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The initial request args object, URL-encoded and appended as "?q=" parameter - * @param options The svelte-query infinite query options object - * @returns useQuery hook - */ -export function useInfiniteModelQuery( - model: string, - url: string, - args: unknown, - options: StoreOrVal< - Omit>, 'queryKey' | 'initialPageParam'> - >, - fetch?: FetchFn -) { - const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); - const queryFn: QueryFunction = ({ pageParam, signal }) => - fetcher(makeUrl(url, pageParam ?? args), { signal }, fetch, false); - - let mergedOpt: StoreOrVal>>; - if ( - isStore< - Omit>, 'queryKey' | 'initialPageParam'> - >(options) - ) { - // options is store - mergedOpt = derived([options], ([$opt]) => { - return { - queryKey, - queryFn, - initialPageParam: args, - ...$opt, - }; - }); - } else { - // options is value - mergedOpt = { - queryKey, - queryFn, - initialPageParam: args, - ...options, - }; - } - - const result = createInfiniteQuery>(mergedOpt); - return derived(result, (r) => ({ - queryKey, - ...r, - })); -} - -function isStore(opt: unknown): opt is Readable { - return typeof (opt as any)?.subscribe === 'function'; -} - -/** - * Creates a POST mutation with svelte-query. - * - * @param model The name of the model under mutation. - * @param method The HTTP method. - * @param modelMeta The model metadata. - * @param url The request URL. - * @param options The svelte-query options. - * @returns useMutation hooks - */ -export function useModelMutation< - TArgs, - TError, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R ->( - model: string, - method: 'POST' | 'PUT' | 'DELETE', - url: string, - modelMeta: ModelMeta, - options?: Omit, 'mutationFn'> & ExtraMutationOptions, - fetch?: FetchFn, - checkReadBack?: C -) { - const queryClient = useQueryClient(); - const mutationFn = (data: any) => { - const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; - const fetchInit: RequestInit = { - method, - ...(method !== 'DELETE' && { - headers: { - 'content-type': 'application/json', - }, - body: marshal(data), - }), - }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; - }; - - const finalOptions = { ...options, mutationFn }; - const operation = url.split('/').pop(); - const invalidateQueries = options?.invalidateQueries !== false; - const optimisticUpdate = !!options?.optimisticUpdate; - - if (operation) { - const { logging } = getContext(SvelteQueryContextKey); - if (invalidateQueries) { - setupInvalidation( - model, - operation, - modelMeta, - finalOptions, - (predicate) => queryClient.invalidateQueries({ predicate }), - logging - ); - } - - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - modelMeta, - finalOptions, - queryClient.getQueryCache().getAll(), - (queryKey, data) => queryClient.setQueryData(queryKey, data), - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging - ); - } - } - - return createMutation(finalOptions); -} diff --git a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts b/packages/plugins/tanstack-query/src/runtime-v5/vue.ts deleted file mode 100644 index bef7e26ae..000000000 --- a/packages/plugins/tanstack-query/src/runtime-v5/vue.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - useInfiniteQuery, - useMutation, - useQuery, - useQueryClient, - type InfiniteData, - type UseInfiniteQueryOptions, - type UseMutationOptions, - type UseQueryOptions, -} from '@tanstack/vue-query'; -import type { ModelMeta } from '@zenstackhq/runtime/cross'; -import { computed, inject, provide, toValue, type ComputedRef, type MaybeRefOrGetter } from 'vue'; -import { - APIContext, - DEFAULT_QUERY_ENDPOINT, - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, -} from '../runtime/common'; - -export { APIContext as RequestHandlerContext } from '../runtime/common'; - -export const VueQueryContextKey = 'zenstack-vue-query-context'; - -/** - * Provide context for the generated TanStack Query hooks. - */ -export function provideHooksContext(context: APIContext) { - provide(VueQueryContextKey, context); -} - -/** - * Hooks context. - */ -export function getHooksContext() { - const { endpoint, ...rest } = inject(VueQueryContextKey, { - endpoint: DEFAULT_QUERY_ENDPOINT, - fetch: undefined, - logging: false, - }); - return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; -} - -/** - * Creates a vue-query query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The request args object, URL-encoded and appended as "?q=" parameter - * @param options The vue-query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook - */ -export function useModelQuery( - model: string, - url: string, - args?: MaybeRefOrGetter | ComputedRef, - options?: - | MaybeRefOrGetter, 'queryKey'> & ExtraQueryOptions> - | ComputedRef, 'queryKey'> & ExtraQueryOptions>, - fetch?: FetchFn -) { - const optionsValue = toValue< - (Omit, 'queryKey'> & ExtraQueryOptions) | undefined - >(options); - const queryKey = getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: optionsValue?.optimisticUpdate !== false, - }); - const queryOptions = computed, 'queryKey'> & ExtraQueryOptions>( - () => { - return { - queryKey, - queryFn: ({ queryKey, signal }) => { - const [_prefix, _model, _op, args] = queryKey; - const reqUrl = makeUrl(url, toValue(args)); - return fetcher(reqUrl, { signal }, fetch, false); - }, - ...optionsValue, - }; - } - ); - return { queryKey, ...useQuery(queryOptions) }; -} - -/** - * Creates a vue-query infinite query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The initial request args object, URL-encoded and appended as "?q=" parameter - * @param options The vue-query infinite query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useInfiniteQuery hook - */ -export function useInfiniteModelQuery( - model: string, - url: string, - args?: MaybeRefOrGetter | ComputedRef, - options?: - | MaybeRefOrGetter< - Omit>, 'queryKey' | 'initialPageParam'> - > - | ComputedRef< - Omit>, 'queryKey' | 'initialPageParam'> - >, - fetch?: FetchFn -) { - // CHECKME: vue-query's `useInfiniteQuery`'s input typing seems wrong - const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); - const queryOptions: any = computed< - Omit>, 'queryKey' | 'initialPageParam'> - >(() => ({ - queryKey, - queryFn: ({ queryKey, pageParam, signal }) => { - const [_prefix, _model, _op, args] = queryKey; - const reqUrl = makeUrl(url, pageParam ?? toValue(args)); - return fetcher(reqUrl, { signal }, fetch, false); - }, - initialPageParam: toValue(args), - ...toValue(options), - })); - - return { queryKey, ...useInfiniteQuery(queryOptions) }; -} - -/** - * Creates a mutation with vue-query. - * - * @param model The name of the model under mutation. - * @param method The HTTP method. - * @param modelMeta The model metadata. - * @param url The request URL. - * @param options The vue-query options. - * @param fetch The fetch function to use for sending the HTTP request - * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @returns useMutation hooks - */ -export function useModelMutation< - TArgs, - TError, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R ->( - model: string, - method: 'POST' | 'PUT' | 'DELETE', - url: string, - modelMeta: ModelMeta, - options?: - | MaybeRefOrGetter< - Omit, 'mutationFn'> & ExtraMutationOptions - > - | ComputedRef, 'mutationFn'> & ExtraMutationOptions>, - fetch?: FetchFn, - checkReadBack?: C -) { - const queryClient = useQueryClient(); - const mutationFn = (data: any) => { - const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; - const fetchInit: RequestInit = { - method, - ...(method !== 'DELETE' && { - headers: { - 'content-type': 'application/json', - }, - body: marshal(data), - }), - }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; - }; - - const optionsValue = toValue< - (Omit, 'mutationFn'> & ExtraMutationOptions) | undefined - >(options); - // TODO: figure out the typing problem - const finalOptions: any = computed(() => ({ ...optionsValue, mutationFn })); - const operation = url.split('/').pop(); - const invalidateQueries = optionsValue?.invalidateQueries !== false; - const optimisticUpdate = !!optionsValue?.optimisticUpdate; - - if (operation) { - const { logging } = getHooksContext(); - if (invalidateQueries) { - setupInvalidation( - model, - operation, - modelMeta, - toValue(finalOptions), - (predicate) => queryClient.invalidateQueries({ predicate }), - logging - ); - } - - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - modelMeta, - toValue(finalOptions), - queryClient.getQueryCache().getAll(), - (queryKey, data) => queryClient.setQueryData(queryKey, data), - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging - ); - } - } - return useMutation(finalOptions); -} diff --git a/packages/plugins/tanstack-query/src/runtime/common.ts b/packages/plugins/tanstack-query/src/runtime/common.ts deleted file mode 100644 index f63a235fe..000000000 --- a/packages/plugins/tanstack-query/src/runtime/common.ts +++ /dev/null @@ -1,477 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { deserialize, serialize } from '@zenstackhq/runtime/browser'; -import { - applyMutation, - getMutatedModels, - getReadModels, - type ModelMeta, - type PrismaWriteActionType, -} from '@zenstackhq/runtime/cross'; - -/** - * The default query endpoint. - */ -export const DEFAULT_QUERY_ENDPOINT = '/api/model'; - -/** - * Prefix for react-query keys. - */ -export const QUERY_KEY_PREFIX = 'zenstack'; - -/** - * Function signature for `fetch`. - */ -export type FetchFn = (url: string, options?: RequestInit) => Promise; - -/** - * Type for query and mutation errors. - */ -export type QueryError = Error & { - /** - * Additional error information. - */ - info?: unknown; - - /** - * HTTP status code. - */ - status?: number; -}; - -/** - * Result of optimistic data provider. - */ -export type OptimisticDataProviderResult = { - /** - * Kind of the result. - * - Update: use the `data` field to update the query cache. - * - Skip: skip the optimistic update for this query. - * - ProceedDefault: proceed with the default optimistic update. - */ - kind: 'Update' | 'Skip' | 'ProceedDefault'; - - /** - * Data to update the query cache. Only applicable if `kind` is 'Update'. - * - * If the data is an object with fields updated, it should have a `$optimistic` - * field set to `true`. If it's an array and an element object is created or updated, - * the element should have a `$optimistic` field set to `true`. - */ - data?: any; -}; - -/** - * Optimistic data provider. - * - * @param args Arguments. - * @param args.queryModel The model of the query. - * @param args.queryOperation The operation of the query, `findMany`, `count`, etc. - * @param args.queryArgs The arguments of the query. - * @param args.currentData The current cache data for the query. - * @param args.mutationArgs The arguments of the mutation. - */ -export type OptimisticDataProvider = (args: { - queryModel: string; - queryOperation: string; - queryArgs: any; - currentData: any; - mutationArgs: any; -}) => OptimisticDataProviderResult | Promise; - -/** - * Extra mutation options. - */ -export type ExtraMutationOptions = { - /** - * Whether to automatically invalidate queries potentially affected by the mutation. Defaults to `true`. - */ - invalidateQueries?: boolean; - - /** - * Whether to optimistically update queries potentially affected by the mutation. Defaults to `false`. - */ - optimisticUpdate?: boolean; - - /** - * A callback for computing optimistic update data for each query cache entry. - */ - optimisticDataProvider?: OptimisticDataProvider; -}; - -/** - * Extra query options. - */ -export type ExtraQueryOptions = { - /** - * Whether to opt-in to optimistic updates for this query. Defaults to `true`. - */ - optimisticUpdate?: boolean; -}; - -/** - * Context type for configuring the hooks. - */ -export type APIContext = { - /** - * The endpoint to use for the queries. - */ - endpoint?: string; - - /** - * A custom fetch function for sending the HTTP requests. - */ - fetch?: FetchFn; - - /** - * If logging is enabled. - */ - logging?: boolean; -}; - -export async function fetcher( - url: string, - options?: RequestInit, - customFetch?: FetchFn, - checkReadBack?: C -): Promise { - // Note: 'cross-fetch' is supposed to handle fetch compatibility - // but it doesn't work for cloudflare workers - const _fetch = - customFetch ?? - // check if fetch is available globally - (typeof fetch === 'function' - ? fetch - : // fallback to 'cross-fetch' if otherwise - (await import('cross-fetch')).default); - - const res = await _fetch(url, options); - if (!res.ok) { - const errData = unmarshal(await res.text()); - if ( - checkReadBack !== false && - errData.error?.prisma && - errData.error?.code === 'P2004' && - errData.error?.reason === 'RESULT_NOT_READABLE' - ) { - // policy doesn't allow mutation result to be read back, just return undefined - return undefined as any; - } - const error: QueryError = new Error('An error occurred while fetching the data.'); - error.info = errData.error; - error.status = res.status; - throw error; - } - - const textResult = await res.text(); - try { - return unmarshal(textResult).data as R; - } catch (err) { - console.error(`Unable to deserialize data:`, textResult); - throw err; - } -} - -type QueryKey = [ - string /* prefix */, - string /* model */, - string /* operation */, - unknown /* args */, - { - infinite: boolean; - optimisticUpdate: boolean; - } /* flags */ -]; - -/** - * Computes query key for the given model, operation and query args. - * @param model Model name. - * @param urlOrOperation Prisma operation (e.g, `findMany`) or request URL. If it's a URL, the last path segment will be used as the operation name. - * @param args Prisma query arguments. - * @param options Query options, including `infinite` indicating if it's an infinite query (defaults to false), and `optimisticUpdate` indicating if optimistic updates are enabled (defaults to true). - * @returns Query key - */ -export function getQueryKey( - model: string, - urlOrOperation: string, - args: unknown, - options: { infinite: boolean; optimisticUpdate: boolean } = { infinite: false, optimisticUpdate: true } -): QueryKey { - if (!urlOrOperation) { - throw new Error('Invalid urlOrOperation'); - } - const operation = urlOrOperation.split('/').pop(); - - const infinite = options.infinite; - // infinite query doesn't support optimistic updates - const optimisticUpdate = options.infinite ? false : options.optimisticUpdate; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return [QUERY_KEY_PREFIX, model, operation!, args, { infinite, optimisticUpdate }]; -} - -export function marshal(value: unknown) { - const { data, meta } = serialize(value); - if (meta) { - return JSON.stringify({ ...(data as any), meta: { serialization: meta } }); - } else { - return JSON.stringify(data); - } -} - -export function unmarshal(value: string) { - const parsed = JSON.parse(value); - if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) { - const deserializedData = deserialize(parsed.data, parsed.meta.serialization); - return { ...parsed, data: deserializedData }; - } else { - return parsed; - } -} - -export function makeUrl(url: string, args: unknown) { - if (!args) { - return url; - } - - const { data, meta } = serialize(args); - let result = `${url}?q=${encodeURIComponent(JSON.stringify(data))}`; - if (meta) { - result += `&meta=${encodeURIComponent(JSON.stringify({ serialization: meta }))}`; - } - return result; -} - -type InvalidationPredicate = ({ queryKey }: { queryKey: readonly unknown[] }) => boolean; -type InvalidateFunc = (predicate: InvalidationPredicate) => Promise; -type MutationOptions = { - onMutate?: (...args: any[]) => any; - onSuccess?: (...args: any[]) => any; - onSettled?: (...args: any[]) => any; -}; - -// sets up invalidation hook for a mutation -export function setupInvalidation( - model: string, - operation: string, - modelMeta: ModelMeta, - options: MutationOptions, - invalidate: InvalidateFunc, - logging = false -) { - const origOnSuccess = options?.onSuccess; - options.onSuccess = async (...args: unknown[]) => { - const [_, variables] = args; - const predicate = await getInvalidationPredicate( - model, - operation as PrismaWriteActionType, - variables, - modelMeta, - logging - ); - await invalidate(predicate); - return origOnSuccess?.(...args); - }; -} - -// gets a predicate for evaluating whether a query should be invalidated -async function getInvalidationPredicate( - model: string, - operation: PrismaWriteActionType, - mutationArgs: any, - modelMeta: ModelMeta, - logging = false -) { - const mutatedModels = await getMutatedModels(model, operation, mutationArgs, modelMeta); - - return ({ queryKey }: { queryKey: readonly unknown[] }) => { - const [_, queryModel, , args] = queryKey as QueryKey; - - if (mutatedModels.includes(queryModel)) { - // direct match - if (logging) { - console.log(`Invalidating query ${JSON.stringify(queryKey)} due to mutation "${model}.${operation}"`); - } - return true; - } - - if (args) { - // traverse query args to find nested reads that match the model under mutation - if (findNestedRead(queryModel, mutatedModels, modelMeta, args)) { - if (logging) { - console.log( - `Invalidating query ${JSON.stringify(queryKey)} due to mutation "${model}.${operation}"` - ); - } - return true; - } - } - - return false; - }; -} - -// find nested reads that match the given models -function findNestedRead(visitingModel: string, targetModels: string[], modelMeta: ModelMeta, args: any) { - const modelsRead = getReadModels(visitingModel, modelMeta, args); - return targetModels.some((m) => modelsRead.includes(m)); -} - -type QueryCache = { - queryKey: readonly unknown[]; - state: { - data: unknown; - error: unknown; - }; -}[]; - -type SetCacheFunc = (queryKey: readonly unknown[], data: unknown) => void; - -/** - * Sets up optimistic update and invalidation (after settled) for a mutation. - */ -export function setupOptimisticUpdate( - model: string, - operation: string, - modelMeta: ModelMeta, - options: MutationOptions & ExtraMutationOptions, - queryCache: QueryCache, - setCache: SetCacheFunc, - invalidate?: InvalidateFunc, - logging = false -) { - const origOnMutate = options?.onMutate; - const origOnSettled = options?.onSettled; - - // optimistic update on mutate - options.onMutate = async (...args: unknown[]) => { - const [variables] = args; - await optimisticUpdate( - model, - operation as PrismaWriteActionType, - variables, - options, - modelMeta, - queryCache, - setCache, - logging - ); - return origOnMutate?.(...args); - }; - - // invalidate on settled - options.onSettled = async (...args: unknown[]) => { - if (invalidate) { - const [, , variables] = args; - const predicate = await getInvalidationPredicate( - model, - operation as PrismaWriteActionType, - variables, - modelMeta, - logging - ); - await invalidate(predicate); - } - return origOnSettled?.(...args); - }; -} - -// optimistically updates query cache -async function optimisticUpdate( - mutationModel: string, - mutationOp: string, - mutationArgs: any, - options: MutationOptions & ExtraMutationOptions, - modelMeta: ModelMeta, - queryCache: QueryCache, - setCache: SetCacheFunc, - logging = false -) { - for (const cacheItem of queryCache) { - const { - queryKey, - state: { data, error }, - } = cacheItem; - - if (!isZenStackQueryKey(queryKey)) { - // skip non-zenstack queries - continue; - } - - if (error) { - if (logging) { - console.warn(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to error:`, error); - } - continue; - } - - const [_, queryModel, queryOperation, queryArgs, queryOptions] = queryKey; - if (!queryOptions?.optimisticUpdate) { - if (logging) { - console.log(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to opt-out`); - } - continue; - } - - if (options.optimisticDataProvider) { - const providerResult = await options.optimisticDataProvider({ - queryModel, - queryOperation, - queryArgs, - currentData: data, - mutationArgs, - }); - - if (providerResult?.kind === 'Skip') { - // skip - if (logging) { - console.log(`Skipping optimistic update for ${JSON.stringify(queryKey)} due to provider`); - } - continue; - } else if (providerResult?.kind === 'Update') { - // update cache - if (logging) { - console.log(`Optimistically updating query ${JSON.stringify(queryKey)} due to provider`); - } - setCache(queryKey, providerResult.data); - continue; - } - } - - // proceed with default optimistic update - const mutatedData = await applyMutation( - queryModel, - queryOperation, - data, - mutationModel, - mutationOp as PrismaWriteActionType, - mutationArgs, - modelMeta, - logging - ); - - if (mutatedData !== undefined) { - // mutation applicable to this query, update cache - if (logging) { - console.log( - `Optimistically updating query ${JSON.stringify( - queryKey - )} due to mutation "${mutationModel}.${mutationOp}"` - ); - } - setCache(queryKey, mutatedData); - } - } -} - -function isZenStackQueryKey(queryKey: readonly unknown[]): queryKey is QueryKey { - if (queryKey.length < 5) { - return false; - } - - if (queryKey[0] !== QUERY_KEY_PREFIX) { - return false; - } - - return true; -} diff --git a/packages/plugins/tanstack-query/src/runtime/index.ts b/packages/plugins/tanstack-query/src/runtime/index.ts deleted file mode 100644 index 085fd5bf3..000000000 --- a/packages/plugins/tanstack-query/src/runtime/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - getQueryKey, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, - type QueryError, -} from './common'; -export * from './prisma-types'; diff --git a/packages/plugins/tanstack-query/src/runtime/prisma-types.ts b/packages/plugins/tanstack-query/src/runtime/prisma-types.ts deleted file mode 120000 index 277b4b133..000000000 --- a/packages/plugins/tanstack-query/src/runtime/prisma-types.ts +++ /dev/null @@ -1 +0,0 @@ -../../../prisma-types.ts \ No newline at end of file diff --git a/packages/plugins/tanstack-query/src/runtime/react.ts b/packages/plugins/tanstack-query/src/runtime/react.ts deleted file mode 100644 index ef7076733..000000000 --- a/packages/plugins/tanstack-query/src/runtime/react.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - useInfiniteQuery, - useMutation, - useQuery, - useQueryClient, - type UseInfiniteQueryOptions, - type UseMutationOptions, - type UseQueryOptions, -} from '@tanstack/react-query'; -import type { ModelMeta } from '@zenstackhq/runtime/cross'; -import { createContext, useContext } from 'react'; -import { - DEFAULT_QUERY_ENDPOINT, - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type APIContext, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, -} from './common'; - -/** - * Context for configuring react hooks. - */ -export const RequestHandlerContext = createContext({ - endpoint: DEFAULT_QUERY_ENDPOINT, - fetch: undefined, - logging: false, -}); - -/** - * Context provider. - */ -export const Provider = RequestHandlerContext.Provider; - -/** - * Hooks context. - */ -export function getHooksContext() { - const { endpoint, ...rest } = useContext(RequestHandlerContext); - return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; -} - -/** - * Creates a react-query query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The request args object, URL-encoded and appended as "?q=" parameter - * @param options The react-query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook - */ -export function useModelQuery( - model: string, - url: string, - args?: unknown, - options?: Omit, 'queryKey'> & ExtraQueryOptions, - fetch?: FetchFn -) { - const reqUrl = makeUrl(url, args); - const queryKey = getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: options?.optimisticUpdate !== false, - }); - return { - queryKey, - ...useQuery({ - queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), - ...options, - }), - }; -} - -/** - * Creates a react-query infinite query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The initial request args object, URL-encoded and appended as "?q=" parameter - * @param options The react-query infinite query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useInfiniteQuery hook - */ -export function useInfiniteModelQuery( - model: string, - url: string, - args?: unknown, - options?: Omit, 'queryKey'>, - fetch?: FetchFn -) { - const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); - return { - queryKey, - ...useInfiniteQuery({ - queryKey, - queryFn: ({ pageParam, signal }) => { - return fetcher(makeUrl(url, pageParam ?? args), { signal }, fetch, false); - }, - ...options, - }), - }; -} - -/** - * Creates a mutation with react-query. - * - * @param model The name of the model under mutation. - * @param method The HTTP method. - * @param modelMeta The model metadata. - * @param url The request URL. - * @param options The react-query options. - * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @returns useMutation hooks - */ -export function useModelMutation< - TArgs, - TError, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R ->( - model: string, - method: 'POST' | 'PUT' | 'DELETE', - url: string, - modelMeta: ModelMeta, - options?: Omit, 'mutationFn'> & ExtraMutationOptions, - fetch?: FetchFn, - checkReadBack?: C -) { - const queryClient = useQueryClient(); - const mutationFn = (data: any) => { - const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; - const fetchInit: RequestInit = { - method, - ...(method !== 'DELETE' && { - headers: { - 'content-type': 'application/json', - }, - body: marshal(data), - }), - }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; - }; - - const finalOptions = { ...options, mutationFn }; - const operation = url.split('/').pop(); - const invalidateQueries = options?.invalidateQueries !== false; - const optimisticUpdate = !!options?.optimisticUpdate; - - if (operation) { - const { logging } = useContext(RequestHandlerContext); - if (invalidateQueries) { - setupInvalidation( - model, - operation, - modelMeta, - finalOptions, - (predicate) => queryClient.invalidateQueries({ predicate }), - logging - ); - } - - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - modelMeta, - finalOptions, - queryClient.getQueryCache().getAll(), - (queryKey, data) => queryClient.setQueryData(queryKey, data), - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging - ); - } - } - - return useMutation(finalOptions); -} diff --git a/packages/plugins/tanstack-query/src/runtime/svelte.ts b/packages/plugins/tanstack-query/src/runtime/svelte.ts deleted file mode 100644 index d3277d526..000000000 --- a/packages/plugins/tanstack-query/src/runtime/svelte.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - createInfiniteQuery, - createMutation, - createQuery, - useQueryClient, - type CreateInfiniteQueryOptions, - type CreateQueryOptions, - type MutationOptions, -} from '@tanstack/svelte-query'; -import { ModelMeta } from '@zenstackhq/runtime/cross'; -import { getContext, setContext } from 'svelte'; -import { derived } from 'svelte/store'; -import { - APIContext, - DEFAULT_QUERY_ENDPOINT, - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, -} from './common'; - -export { APIContext as RequestHandlerContext } from './common'; - -/** - * Key for setting and getting the global query context. - */ -export const SvelteQueryContextKey = 'zenstack-svelte-query-context'; - -/** - * Set context for the generated TanStack Query hooks. - */ -export function setHooksContext(context: APIContext) { - setContext(SvelteQueryContextKey, context); -} - -/** - * Hooks context. - */ -export function getHooksContext() { - const { endpoint, ...rest } = getContext(SvelteQueryContextKey); - return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; -} - -/** - * Creates a svelte-query query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The request args object, URL-encoded and appended as "?q=" parameter - * @param options The svelte-query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook - */ -export function useModelQuery( - model: string, - url: string, - args?: unknown, - options?: Omit, 'queryKey'> & ExtraQueryOptions, - fetch?: FetchFn -) { - const reqUrl = makeUrl(url, args); - const queryKey = getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: options?.optimisticUpdate !== false, - }); - const result = createQuery({ - queryKey, - queryFn: ({ signal }) => fetcher(reqUrl, { signal }, fetch, false), - ...options, - }); - return derived(result, (r) => ({ - queryKey, - ...r, - })); -} - -/** - * Creates a svelte-query infinite query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The initial request args object, URL-encoded and appended as "?q=" parameter - * @param options The svelte-query infinite query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook - */ -export function useInfiniteModelQuery( - model: string, - url: string, - args?: unknown, - options?: Omit, 'queryKey'>, - fetch?: FetchFn -) { - const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); - const result = createInfiniteQuery({ - queryKey, - queryFn: ({ pageParam, signal }) => - fetcher(makeUrl(url, pageParam ?? args), { signal }, fetch, false), - ...options, - }); - return derived(result, (r) => ({ - queryKey, - ...r, - })); -} - -/** - * Creates a POST mutation with svelte-query. - * - * @param model The name of the model under mutation. - * @param method The HTTP method. - * @param modelMeta The model metadata. - * @param url The request URL. - * @param options The svelte-query options. - * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @returns useMutation hooks - */ -export function useModelMutation< - TArgs, - TError, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R ->( - model: string, - method: 'POST' | 'PUT' | 'DELETE', - url: string, - modelMeta: ModelMeta, - options?: Omit, 'mutationFn'> & ExtraMutationOptions, - fetch?: FetchFn, - checkReadBack?: C -) { - const queryClient = useQueryClient(); - const mutationFn = (data: any) => { - const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; - const fetchInit: RequestInit = { - method, - ...(method !== 'DELETE' && { - headers: { - 'content-type': 'application/json', - }, - body: marshal(data), - }), - }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; - }; - - const finalOptions = { ...options, mutationFn }; - const operation = url.split('/').pop(); - const invalidateQueries = options?.invalidateQueries !== false; - const optimisticUpdate = !!options?.optimisticUpdate; - - if (operation) { - const { logging } = getContext(SvelteQueryContextKey); - - if (invalidateQueries) { - setupInvalidation( - model, - operation, - modelMeta, - finalOptions, - (predicate) => queryClient.invalidateQueries({ predicate }), - logging - ); - } - - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - modelMeta, - finalOptions, - queryClient.getQueryCache().getAll(), - (queryKey, data) => queryClient.setQueryData(queryKey, data), - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging - ); - } - } - - return createMutation(finalOptions); -} diff --git a/packages/plugins/tanstack-query/src/runtime/vue.ts b/packages/plugins/tanstack-query/src/runtime/vue.ts deleted file mode 100644 index ce5ad0693..000000000 --- a/packages/plugins/tanstack-query/src/runtime/vue.ts +++ /dev/null @@ -1,215 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - useInfiniteQuery, - useMutation, - useQuery, - useQueryClient, - type InfiniteData, - type UseInfiniteQueryOptions, - type UseMutationOptions, - type UseQueryOptions, -} from '@tanstack/vue-query'; -import type { ModelMeta } from '@zenstackhq/runtime/cross'; -import { computed, inject, provide, toValue, type ComputedRef, type MaybeRefOrGetter } from 'vue'; -import { - APIContext, - DEFAULT_QUERY_ENDPOINT, - fetcher, - getQueryKey, - makeUrl, - marshal, - setupInvalidation, - setupOptimisticUpdate, - type ExtraMutationOptions, - type ExtraQueryOptions, - type FetchFn, -} from './common'; - -export { APIContext as RequestHandlerContext } from './common'; - -export const VueQueryContextKey = 'zenstack-vue-query-context'; - -/** - * Provide context for the generated TanStack Query hooks. - */ -export function provideHooksContext(context: APIContext) { - provide(VueQueryContextKey, context); -} - -/** - * Hooks context. - */ -export function getHooksContext() { - const { endpoint, ...rest } = inject(VueQueryContextKey, { - endpoint: DEFAULT_QUERY_ENDPOINT, - fetch: undefined, - logging: false, - }); - return { endpoint: endpoint ?? DEFAULT_QUERY_ENDPOINT, ...rest }; -} - -/** - * Creates a vue-query query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The request args object, URL-encoded and appended as "?q=" parameter - * @param options The vue-query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useQuery hook - */ -export function useModelQuery( - model: string, - url: string, - args?: MaybeRefOrGetter | ComputedRef, - options?: - | MaybeRefOrGetter, 'queryKey'> & ExtraQueryOptions> - | ComputedRef, 'queryKey'> & ExtraQueryOptions>, - fetch?: FetchFn -) { - const optionsValue = toValue< - (Omit, 'queryKey'> & ExtraQueryOptions) | undefined - >(options); - const queryKey = getQueryKey(model, url, args, { - infinite: false, - optimisticUpdate: optionsValue?.optimisticUpdate !== false, - }); - const queryOptions = computed, 'queryKey'>>(() => { - return { - queryKey, - queryFn: ({ queryKey, signal }) => { - const [_prefix, _model, _op, args] = queryKey; - const reqUrl = makeUrl(url, toValue(args)); - return fetcher(reqUrl, { signal }, fetch, false); - }, - ...optionsValue, - }; - }); - return { queryKey, ...useQuery(queryOptions) }; -} - -/** - * Creates a vue-query infinite query. - * - * @param model The name of the model under query. - * @param url The request URL. - * @param args The initial request args object, URL-encoded and appended as "?q=" parameter - * @param options The vue-query infinite query options object - * @param fetch The fetch function to use for sending the HTTP request - * @returns useInfiniteQuery hook - */ -export function useInfiniteModelQuery( - model: string, - url: string, - args?: MaybeRefOrGetter | ComputedRef, - options?: - | MaybeRefOrGetter< - Omit>, 'queryKey' | 'initialPageParam'> - > - | ComputedRef< - Omit>, 'queryKey' | 'initialPageParam'> - >, - fetch?: FetchFn -) { - // CHECKME: vue-query's `useInfiniteQuery`'s input typing seems wrong - const queryKey = getQueryKey(model, url, args, { infinite: true, optimisticUpdate: false }); - const queryOptions: any = computed< - Omit>, 'queryKey' | 'initialPageParam'> - >(() => ({ - queryKey, - queryFn: ({ queryKey, pageParam, signal }) => { - const [_prefix, _model, _op, args] = queryKey; - const reqUrl = makeUrl(url, pageParam ?? toValue(args)); - return fetcher(reqUrl, { signal }, fetch, false); - }, - initialPageParam: toValue(args), - ...toValue(options), - })); - - return { queryKey, ...useInfiniteQuery(queryOptions) }; -} - -/** - * Creates a mutation with vue-query. - * - * @param model The name of the model under mutation. - * @param method The HTTP method. - * @param modelMeta The model metadata. - * @param url The request URL. - * @param options The vue-query options. - * @param fetch The fetch function to use for sending the HTTP request - * @param checkReadBack Whether to check for read back errors and return undefined if found. - * @returns useMutation hooks - */ -export function useModelMutation< - TArgs, - TError, - R = any, - C extends boolean = boolean, - Result = C extends true ? R | undefined : R ->( - model: string, - method: 'POST' | 'PUT' | 'DELETE', - url: string, - modelMeta: ModelMeta, - options?: - | MaybeRefOrGetter< - Omit, 'mutationFn'> & ExtraMutationOptions - > - | ComputedRef, 'mutationFn'> & ExtraMutationOptions>, - fetch?: FetchFn, - checkReadBack?: C -) { - const queryClient = useQueryClient(); - const mutationFn = (data: any) => { - const reqUrl = method === 'DELETE' ? makeUrl(url, data) : url; - const fetchInit: RequestInit = { - method, - ...(method !== 'DELETE' && { - headers: { - 'content-type': 'application/json', - }, - body: marshal(data), - }), - }; - return fetcher(reqUrl, fetchInit, fetch, checkReadBack) as Promise; - }; - - const optionsValue = toValue< - (Omit, 'mutationFn'> & ExtraMutationOptions) | undefined - >(options); - // TODO: figure out the typing problem - const finalOptions: any = computed(() => ({ ...optionsValue, mutationFn })); - const operation = url.split('/').pop(); - const invalidateQueries = optionsValue?.invalidateQueries !== false; - const optimisticUpdate = !!optionsValue?.optimisticUpdate; - - if (operation) { - const { logging } = getHooksContext(); - if (invalidateQueries) { - setupInvalidation( - model, - operation, - modelMeta, - toValue(finalOptions), - (predicate) => queryClient.invalidateQueries({ predicate }), - logging - ); - } - - if (optimisticUpdate) { - setupOptimisticUpdate( - model, - operation, - modelMeta, - toValue(finalOptions), - queryClient.getQueryCache().getAll(), - (queryKey, data) => queryClient.setQueryData(queryKey, data), - invalidateQueries ? (predicate) => queryClient.invalidateQueries({ predicate }) : undefined, - logging - ); - } - } - return useMutation(finalOptions); -} diff --git a/packages/plugins/tanstack-query/tests/plugin.test.ts b/packages/plugins/tanstack-query/tests/plugin.test.ts deleted file mode 100644 index 1c72d063b..000000000 --- a/packages/plugins/tanstack-query/tests/plugin.test.ts +++ /dev/null @@ -1,414 +0,0 @@ -/// - -import { loadSchema, normalizePath } from '@zenstackhq/testtools'; -import fs from 'fs'; -import path from 'path'; -import tmp from 'tmp'; - -describe('Tanstack Query Plugin Tests', () => { - let origDir: string; - - beforeAll(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - const sharedModel = ` -model User { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - role role @default(USER) - posts post_Item[] -} - -enum role { - USER - ADMIN -} - -model post_Item { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id], onDelete: Cascade) - authorId String? - published Boolean @default(false) - viewCount Int @default(0) -} - -model Foo { - id String @id - @@ignore -} - `; - - const reactAppSource = { - name: 'main.ts', - content: ` - import { useFindFirstpost_Item, useInfiniteFindManypost_Item, useCreatepost_Item } from './hooks'; - - function query() { - const { data, queryKey } = useFindFirstpost_Item({include: { author: true }}, { enabled: true, optimisticUpdate: false }); - console.log(queryKey); - console.log(data?.viewCount); - console.log(data?.author?.email); - } - - function infiniteQuery() { - const { data, queryKey, fetchNextPage, hasNextPage } = useInfiniteFindManypost_Item(); - useInfiniteFindManypost_Item({ where: { published: true } }); - useInfiniteFindManypost_Item(undefined, { enabled: true, getNextPageParam: () => null }); - console.log(queryKey); - console.log(data?.pages[0]?.[0]?.published); - console.log(data?.pageParams[0]); - } - - async function mutation() { - const { mutateAsync } = useCreatepost_Item(); - const data = await mutateAsync({ data: { title: 'hello' }, include: { author: true } }); - console.log(data?.viewCount); - console.log(data?.author?.email); - } - `, - }; - - it('react-query run plugin v4', async () => { - await loadSchema( - ` -plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'react' - version = 'v4' -} - -${sharedModel} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@4.29.7'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [reactAppSource], - } - ); - }); - - it('react-query run plugin v5', async () => { - await loadSchema( - ` -plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'react' -} - -${sharedModel} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [ - reactAppSource, - { - name: 'suspense.ts', - content: ` - import { useSuspenseInfiniteFindManypost_Item } from './hooks'; - - function suspenseInfiniteQuery() { - const { data, queryKey, fetchNextPage, hasNextPage } = useSuspenseInfiniteFindManypost_Item(); - useSuspenseInfiniteFindManypost_Item({ where: { published: true } }); - useSuspenseInfiniteFindManypost_Item(undefined, { getNextPageParam: () => null }); - console.log(queryKey); - console.log(data?.pages[0]?.[0]?.published); - console.log(data?.pageParams[0]); - } - `, - }, - ], - } - ); - }); - - const vueAppSource = { - name: 'main.ts', - content: ` - import { useFindFirstpost_Item, useInfiniteFindManypost_Item, useCreatepost_Item } from './hooks'; - - function query() { - const { data, queryKey } = useFindFirstpost_Item({include: { author: true }}, { enabled: true, optimisticUpdate: false }); - console.log(queryKey); - console.log(data.value?.viewCount); - console.log(data.value?.author?.email); - } - - function infiniteQuery() { - const { data, queryKey, fetchNextPage, hasNextPage } = useInfiniteFindManypost_Item(); - useInfiniteFindManypost_Item({ where: { published: true } }, { enabled: true, getNextPageParam: () => null }); - useInfiniteFindManypost_Item(undefined, { getNextPageParam: () => null }); - console.log(queryKey); - console.log(data.value?.pages[0]?.[0]?.published); - console.log(data.value?.pageParams[0]); - } - - async function mutation() { - const { mutateAsync } = useCreatepost_Item(); - const data = await mutateAsync({ data: { title: 'hello' }, include: { author: true } }); - console.log(data?.viewCount); - console.log(data?.author?.email); - } - `, - }; - - it('vue-query run plugin v4', async () => { - await loadSchema( - ` -plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'vue' - version = 'v4' -} - -${sharedModel} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@4.37.0'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [vueAppSource], - } - ); - }); - - it('vue-query run plugin v5', async () => { - await loadSchema( - ` -plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'vue' -} - -${sharedModel} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['vue@^3.3.4', '@tanstack/vue-query@latest'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [vueAppSource], - } - ); - }); - - const svelteAppSource = { - name: 'main.ts', - content: ` - import { get } from 'svelte/store'; - import { useFindFirstpost_Item, useInfiniteFindManypost_Item, useCreatepost_Item } from './hooks'; - - function query() { - const { data, queryKey } = get(useFindFirstpost_Item({include: { author: true }}, { enabled: true, optimisticUpdate: false })); - console.log(queryKey); - console.log(data?.viewCount); - console.log(data?.author?.email); - } - - function infiniteQuery() { - const { data, queryKey, fetchNextPage, hasNextPage } = get(useInfiniteFindManypost_Item()); - useInfiniteFindManypost_Item({ where: { published: true } }); - useInfiniteFindManypost_Item(undefined, { enabled: true, getNextPageParam: () => null }); - console.log(queryKey); - console.log(data?.pages[0]?.[0]?.published); - console.log(data?.pageParams[0]); - } - - async function mutation() { - const { mutateAsync } = get(useCreatepost_Item()); - const data = await mutateAsync({ data: { title: 'hello' }, include: { author: true } }); - console.log(data?.viewCount); - console.log(data?.author?.email); - } - `, - }; - - it('svelte-query run plugin v4', async () => { - await loadSchema( - ` -plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'svelte' - version = 'v4' -} - -${sharedModel} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@4.29.7'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [svelteAppSource], - } - ); - }); - - it('svelte-query run plugin v5', async () => { - await loadSchema( - ` -plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'svelte' -} - -${sharedModel} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['svelte@^3.0.0', '@tanstack/svelte-query@^5.0.0'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [svelteAppSource], - } - ); - }); - - const angularAppSource = { - name: 'main.ts', - content: ` - import { Component, inject } from '@angular/core'; - import { useFindFirstpost_Item, useInfiniteFindManypost_Item, useCreatepost_Item } from './hooks'; - - @Component({ - selector: 'app-test', - template: '
Test Component
' - }) - export class TestComponent { - query() { - const { data } = useFindFirstpost_Item({include: { author: true }}, { enabled: true, optimisticUpdate: false }); - console.log(data()?.viewCount); - console.log(data()?.author?.email); - } - - infiniteQuery() { - const { data, fetchNextPage, hasNextPage } = useInfiniteFindManypost_Item(); - useInfiniteFindManypost_Item({ where: { published: true } }); - useInfiniteFindManypost_Item(undefined, { enabled: true, getNextPageParam: () => null }); - console.log(data()?.pages[0][0].published); - console.log(data()?.pageParams[0]); - } - - async mutation() { - const { mutateAsync } = useCreatepost_Item(); - const data = await mutateAsync({ data: { title: 'hello' }, include: { author: true } }); - console.log(data?.viewCount); - console.log(data?.author?.email); - } - } - `, - }; - - it('angular-query run plugin v5', async () => { - await loadSchema( - ` -plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'angular' -} - -${sharedModel} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: [ - '@angular/core@^20.0.0', - '@angular/common@^20.0.0', - '@angular/platform-browser@^20.0.0', - '@tanstack/angular-query-experimental@5.84.x', - 'rxjs@^7.8.0', - 'zone.js@^0.15.0', - ], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [angularAppSource], - } - ); - }); - - it('clear output', async () => { - const { name: projectDir } = tmp.dirSync(); - fs.mkdirSync(path.join(projectDir, 'tanstack'), { recursive: true }); - fs.writeFileSync(path.join(projectDir, 'tanstack', 'test.txt'), 'hello'); - - await loadSchema( - ` - plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/tanstack' - target = 'react' - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - password String @omit - } - `, - { - pushDb: false, - projectDir, - extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`], - } - ); - - expect(fs.existsSync(path.join(projectDir, 'tanstack', 'test.txt'))).toBeFalsy(); - }); - - it('existing output as file', async () => { - const { name: projectDir } = tmp.dirSync(); - fs.writeFileSync(path.join(projectDir, 'tanstack'), 'hello'); - - await expect( - loadSchema( - ` - plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/tanstack' - target = 'react' - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String - password String @omit - } - `, - { pushDb: false, projectDir, extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`] } - ) - ).rejects.toThrow('already exists and is not a directory'); - }); -}); diff --git a/packages/plugins/tanstack-query/tests/portable.test.ts b/packages/plugins/tanstack-query/tests/portable.test.ts deleted file mode 100644 index aa22abf8a..000000000 --- a/packages/plugins/tanstack-query/tests/portable.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -/// - -import { loadSchema, normalizePath } from '@zenstackhq/testtools'; -import path from 'path'; -import tmp from 'tmp'; - -describe('Tanstack Query Plugin Portable Tests', () => { - it('supports portable for standard prisma client', async () => { - await loadSchema( - ` - plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'react' - portable = true - } - - model User { - id Int @id @default(autoincrement()) - email String - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int - } - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { useFindUniqueUser } from './hooks'; -const { data } = useFindUniqueUser({ where: { id: 1 }, include: { posts: true } }); -console.log(data?.email); -console.log(data?.posts[0]?.title); -`, - }, - ], - } - ); - }); - - it('supports portable for custom prisma client output', async () => { - const t = tmp.dirSync({ unsafeCleanup: true }); - const projectDir = t.name; - - await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator client { - provider = 'prisma-client-js' - output = '$projectRoot/myprisma' - } - - plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'react' - portable = true - } - - model User { - id Int @id @default(autoincrement()) - email String - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int - } - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - addPrelude: false, - projectDir, - prismaLoadPath: `${projectDir}/myprisma`, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { useFindUniqueUser } from './hooks'; -const { data } = useFindUniqueUser({ where: { id: 1 }, include: { posts: true } }); -console.log(data?.email); -console.log(data?.posts[0]?.title); -`, - }, - ], - } - ); - }); - - it('supports portable for logical client', async () => { - await loadSchema( - ` - plugin tanstack { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/hooks' - target = 'react' - portable = true - } - - model Base { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - type String - @@delegate(type) - } - - model User extends Base { - email String - } - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: ['react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@5.56.x'], - copyDependencies: [path.resolve(__dirname, '../dist')], - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { useFindUniqueUser } from './hooks'; -const { data } = useFindUniqueUser({ where: { id: 1 } }); -console.log(data?.email); -console.log(data?.createdAt); -`, - }, - ], - } - ); - }); -}); diff --git a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx deleted file mode 100644 index 3559f4528..000000000 --- a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx +++ /dev/null @@ -1,1883 +0,0 @@ -/** - * @jest-environment jsdom - */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query-v5'; -import { act, renderHook, waitFor } from '@testing-library/react'; -import nock from 'nock'; -import React from 'react'; -import { RequestHandlerContext, useInfiniteModelQuery, useModelMutation, useModelQuery } from '../src/runtime-v5/react'; -import { getQueryKey } from '../src/runtime/common'; -import { modelMeta } from './test-model-meta'; - -const BASE_URL = 'http://localhost'; - -describe('Tanstack Query React Hooks V5 Test', () => { - function createWrapper() { - const queryClient = new QueryClient(); - const Provider = RequestHandlerContext.Provider; - const wrapper = ({ children }: { children: React.ReactElement }) => ( - - {/* @ts-ignore */} - {children} - - ); - return { queryClient, wrapper }; - } - - function makeUrl(model: string, operation: string, args?: unknown) { - let r = `${BASE_URL}/api/model/${model}/${operation}`; - if (args) { - r += `?q=${encodeURIComponent(JSON.stringify(args))}`; - } - return r; - } - - beforeEach(() => { - nock.cleanAll(); - }); - - it('simple query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)).get(/.*/).reply(200, { - data, - }); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toMatchObject(data); - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject(data); - }); - }); - - it('infinite query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Query findMany:', queryArgs); - return { - data: data, - }; - }); - - const { result } = renderHook( - () => - useInfiniteModelQuery('User', makeUrl('User', 'findMany'), queryArgs, { - getNextPageParam: () => null, - }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - const resultData = result.current.data!; - expect(resultData.pages).toHaveLength(1); - expect(resultData.pages[0]).toMatchObject(data); - expect(resultData?.pageParams).toHaveLength(1); - expect(resultData?.pageParams[0]).toMatchObject(queryArgs); - expect(result.current.hasNextPage).toBe(false); - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }) - ); - expect(cacheData.pages[0]).toMatchObject(data); - }); - }); - - it('independent mutation and query', async () => { - const { wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - let queryCount = 0; - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - queryCount++; - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('Post', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - return { data: { id: '1', title: 'post1' } }; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('Post', 'POST', makeUrl('Post', 'create'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, data: { title: 'post1' } })); - - await waitFor(() => { - // no refetch caused by invalidation - expect(queryCount).toBe(1); - }); - }); - - it('create and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findMany')), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '1', name: 'foo' }); - return { data: data[0] }; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(1); - }); - }); - - it('create and no invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findMany')), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '1', name: 'foo' }); - return { data: data[0] }; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, undefined, undefined, false), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(0); - }); - }); - - it('optimistic create single', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].name).toBe('foo'); - }); - }); - - it('optimistic create updating nested query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = [{ id: '1', name: 'user1', posts: [] }]; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => - useModelQuery( - 'User', - makeUrl('User', 'findMany'), - { include: { posts: true } }, - { optimisticUpdate: true } - ), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('Post', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('Post', 'POST', makeUrl('Post', 'create'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { title: 'post1', owner: { connect: { id: '1' } } } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'User', - 'findMany', - { include: { posts: true } }, - { infinite: false, optimisticUpdate: true } - ) - ); - const posts = cacheData[0].posts; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ $optimistic: true, id: expect.any(String), title: 'post1', ownerId: '1' }); - }); - }); - - it('optimistic create updating deeply nested query', async () => { - const { queryClient, wrapper } = createWrapper(); - - // populate the cache with a user - - const userData: any[] = [{ id: '1', name: 'user1', posts: [] }]; - - nock(BASE_URL) - .get('/api/model/User/findMany') - .query(true) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(userData)); - return { data: userData }; - }) - .persist(); - - const { result: userResult } = renderHook( - () => - useModelQuery( - 'User', - makeUrl('User', 'findMany'), - { - include: { - posts: { - include: { - category: true, - }, - }, - }, - }, - { optimisticUpdate: true } - ), - { - wrapper, - } - ); - await waitFor(() => { - expect(userResult.current.data).toHaveLength(1); - }); - - // populate the cache with a category - const categoryData: any[] = [{ id: '1', name: 'category1', posts: [] }]; - - nock(BASE_URL) - .get('/api/model/Category/findMany') - .query(true) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(categoryData)); - return { data: categoryData }; - }) - .persist(); - - const { result: categoryResult } = renderHook( - () => - useModelQuery( - 'Category', - makeUrl('Category', 'findMany'), - { include: { posts: true } }, - { optimisticUpdate: true } - ), - { - wrapper, - } - ); - await waitFor(() => { - expect(categoryResult.current.data).toHaveLength(1); - }); - - // create a post and connect it to the category - nock(BASE_URL) - .post('/api/model/Post/create') - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('Post', 'POST', makeUrl('Post', 'create'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - data: { title: 'post1', owner: { connect: { id: '1' } }, category: { connect: { id: '1' } } }, - }) - ); - - // assert that the post was created and connected to the category - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'Category', - 'findMany', - { - include: { - posts: true, - }, - }, - { infinite: false, optimisticUpdate: true } - ) - ); - const posts = cacheData[0].posts; - expect(posts).toHaveLength(1); - console.log('category.posts', posts[0]); - expect(posts[0]).toMatchObject({ - $optimistic: true, - id: expect.any(String), - title: 'post1', - ownerId: '1', - }); - }); - - // assert that the post was created and connected to the user, and included the category - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'User', - 'findMany', - { - include: { - posts: { - include: { - category: true, - }, - }, - }, - }, - { infinite: false, optimisticUpdate: true } - ) - ); - const posts = cacheData[0].posts; - expect(posts).toHaveLength(1); - console.log('user.posts', posts[0]); - expect(posts[0]).toMatchObject({ - $optimistic: true, - id: expect.any(String), - title: 'post1', - ownerId: '1', - categoryId: '1', - // TODO: should this include the category object and not just the foreign key? - // category: { $optimistic: true, id: '1', name: 'category1' }, - }); - }); - }); - - it('optimistic update with optional one-to-many relationship', async () => { - const { queryClient, wrapper } = createWrapper(); - - // populate the cache with a post, with an optional category relationship - const postData: any = { - id: '1', - title: 'post1', - ownerId: '1', - categoryId: null, - category: null, - }; - - const data: any[] = [postData]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .query(true) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result: postResult } = renderHook( - () => - useModelQuery( - 'Post', - makeUrl('Post', 'findMany'), - { - include: { - category: true, - }, - }, - { optimisticUpdate: true } - ), - { - wrapper, - } - ); - await waitFor(() => { - expect(postResult.current.data).toHaveLength(1); - }); - - // mock a put request to update the post title - nock(makeUrl('Post', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - postData.title = 'postA'; - return { data: postData }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('Post', 'PUT', makeUrl('Post', 'update'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } })); - - // assert that the post was updated despite the optional (null) category relationship - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'Post', - 'findMany', - { - include: { - category: true, - }, - }, - { infinite: false, optimisticUpdate: true } - ) - ); - const posts = cacheData; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ - $optimistic: true, - id: expect.any(String), - title: 'postA', - ownerId: '1', - categoryId: null, - category: null, - }); - }); - }); - - it('optimistic update with nested optional one-to-many relationship', async () => { - const { queryClient, wrapper } = createWrapper(); - - // populate the cache with a user and a post, with an optional category - const postData: any = { - id: '1', - title: 'post1', - ownerId: '1', - categoryId: null, - category: null, - }; - - const userData: any[] = [{ id: '1', name: 'user1', posts: [postData] }]; - - nock(BASE_URL) - .get('/api/model/User/findMany') - .query(true) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(userData)); - return { data: userData }; - }) - .persist(); - - const { result: userResult } = renderHook( - () => - useModelQuery( - 'User', - makeUrl('User', 'findMany'), - { - include: { - posts: { - include: { - category: true, - }, - }, - }, - }, - { optimisticUpdate: true } - ), - { - wrapper, - } - ); - await waitFor(() => { - expect(userResult.current.data).toHaveLength(1); - }); - - // mock a put request to update the post title - nock(BASE_URL) - .put('/api/model/Post/update') - .reply(200, () => { - console.log('Mutating data'); - postData.title = 'postA'; - return { data: postData }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('Post', 'PUT', makeUrl('Post', 'update'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } })); - - // assert that the post was updated - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'User', - 'findMany', - { - include: { - posts: { - include: { - category: true, - }, - }, - }, - }, - { infinite: false, optimisticUpdate: true } - ) - ); - const posts = cacheData[0].posts; - expect(posts).toHaveLength(1); - console.log('user.posts', posts[0]); - expect(posts[0]).toMatchObject({ - $optimistic: true, - id: expect.any(String), - title: 'postA', - ownerId: '1', - categoryId: null, - category: null, - }); - }); - }); - - it('optimistic nested create updating query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('Post', makeUrl('Post', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { name: 'user1', posts: { create: { title: 'post1' } } } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].title).toBe('post1'); - }); - }); - - it('optimistic create many', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'createMany')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'POST', makeUrl('User', 'createMany'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: [{ name: 'foo' }, { name: 'bar' }] })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(2); - }); - }); - - it('update and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.name = 'bar'; - return data; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject({ name: 'bar' }); - }); - }); - - it('update and no invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.name = 'bar'; - return data; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, undefined, undefined, false), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject({ name: 'foo' }); - }); - }); - - it('optimistic update simple', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return data; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData( - getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); - }); - }); - - it('optimistic update updating nested query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' }, include: { posts: true } }; - const data = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('Post', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return data; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('Post', 'PUT', makeUrl('Post', 'update'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - where: { id: 'p1' }, - data: { title: 'post2', owner: { connect: { id: '2' } } }, - }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData.posts[0]).toMatchObject({ title: 'post2', $optimistic: true, ownerId: '2' }); - }); - }); - - it('optimistic nested update updating query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: 'p1' } }; - const data = { id: 'p1', title: 'post1' }; - - nock(makeUrl('Post', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('Post', makeUrl('Post', 'findUnique'), queryArgs, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ title: 'post1' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return data; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - data: { posts: { update: { where: { id: 'p1' }, data: { title: 'post2' } } } }, - }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toMatchObject({ title: 'post2', $optimistic: true }); - }); - }); - - it('optimistic upsert - create simple', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'upsert')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'POST', makeUrl('User', 'upsert'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - create: { id: '1', name: 'foo' }, - update: { name: 'bar' }, - }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0]).toMatchObject({ id: '1', name: 'foo', $optimistic: true }); - }); - }); - - it('optimistic upsert - create updating nested query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => - useModelQuery( - 'User', - makeUrl('User', 'findUnique'), - { where: { id: '1' } }, - { optimisticUpdate: true } - ), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ id: '1' }); - }); - - nock(makeUrl('Post', 'upsert')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('Post', 'POST', makeUrl('Post', 'upsert'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - where: { id: 'p2' }, - create: { id: 'p2', title: 'post2', owner: { connect: { id: '1' } } }, - update: { title: 'post3' }, - }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }) - ); - const posts = cacheData.posts; - expect(posts).toHaveLength(2); - expect(posts[0]).toMatchObject({ id: 'p2', title: 'post2', ownerId: '1', $optimistic: true }); - }); - }); - - it('optimistic upsert - nested create updating query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any = [{ id: 'p1', title: 'post1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('Post', makeUrl('Post', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'update')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - data: { - posts: { - upsert: { - where: { id: 'p2' }, - create: { id: 'p2', title: 'post2', owner: { connect: { id: '1' } } }, - update: { title: 'post3', owner: { connect: { id: '2' } } }, - }, - }, - }, - }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(2); - expect(cacheData[0]).toMatchObject({ id: 'p2', title: 'post2', ownerId: '1', $optimistic: true }); - }); - }); - - it('optimistic upsert - update simple', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'upsert')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return data; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'POST', makeUrl('User', 'upsert'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, update: { name: 'bar' }, create: { name: 'zee' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData( - getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); - }); - }); - - it('optimistic upsert - update updating nested query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => - useModelQuery( - 'User', - makeUrl('User', 'findUnique'), - { where: { id: '1' } }, - { optimisticUpdate: true } - ), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ id: '1' }); - }); - - nock(makeUrl('Post', 'upsert')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('Post', 'POST', makeUrl('Post', 'upsert'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - where: { id: 'p1' }, - create: { id: 'p1', title: 'post1' }, - update: { title: 'post2' }, - }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }) - ); - const posts = cacheData.posts; - expect(posts).toHaveLength(1); - expect(posts[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true }); - }); - }); - - it('optimistic upsert - nested update updating query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any = [{ id: 'p1', title: 'post1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('Post', makeUrl('Post', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'update')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - data: { - posts: { - upsert: { - where: { id: 'p1' }, - create: { id: 'p1', title: 'post1' }, - update: { title: 'post2' }, - }, - }, - }, - }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true }); - }); - }); - - it('delete and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findMany')), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.splice(0, 1); - return { data: [] }; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'DELETE', makeUrl('User', 'delete'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(0); - }); - }); - - it('optimistic delete simple', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'DELETE', makeUrl('User', 'delete'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(0); - }); - }); - - it('optimistic delete nested query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] }; - - nock(makeUrl('User', 'findFirst')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => - useModelQuery( - 'User', - makeUrl('User', 'findFirst'), - { include: { posts: true } }, - { optimisticUpdate: true } - ), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ id: '1' }); - }); - - nock(makeUrl('Post', 'delete')) - .delete(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('Post', 'DELETE', makeUrl('Post', 'delete'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: 'p1' } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey( - 'User', - 'findFirst', - { include: { posts: true } }, - { infinite: false, optimisticUpdate: true } - ) - ); - expect(cacheData.posts).toHaveLength(0); - }); - }); - - it('optimistic nested delete update query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any = [ - { id: 'p1', title: 'post1' }, - { id: 'p2', title: 'post2' }, - ]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('Post', makeUrl('Post', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(2); - }); - - nock(makeUrl('User', 'update')) - .delete(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { delete: { id: 'p1' } } } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(1); - }); - }); - - it('top-level mutation and nested-read invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' }, include: { posts: true } }; - const data = { posts: [{ id: '1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('Post', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.posts[0].title = 'post2'; - return data; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('Post', 'PUT', makeUrl('Post', 'update'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { name: 'post2' } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData.posts[0].title).toBe('post2'); - }); - }); - - it('nested mutation and top-level-read invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data = [{ id: '1', title: 'post1', ownerId: '1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('Post', makeUrl('Post', 'findMany')), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '2', title: 'post2', ownerId: '1' }); - return data; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { create: { title: 'post2' } } } }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); - expect(cacheData).toHaveLength(2); - }); - }); - - it('optimistic create with custom provider', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }) - .persist(); - - const { result: mutationResult1 } = renderHook( - () => - useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - optimisticDataProvider: ({ queryModel, queryOperation }) => { - if (queryModel === 'User' && queryOperation === 'findMany') { - return { kind: 'Skip' }; - } else { - return { kind: 'ProceedDefault' }; - } - }, - }), - { - wrapper, - } - ); - - act(() => mutationResult1.current.mutate({ data: { name: 'foo' } })); - - // cache should not update - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(0); - }); - - const { result: mutationResult2 } = renderHook( - () => - useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - optimisticDataProvider: ({ queryModel, queryOperation, currentData, mutationArgs }) => { - if (queryModel === 'User' && queryOperation === 'findMany') { - return { - kind: 'Update', - data: [ - ...currentData, - { id: 100, name: mutationArgs.data.name + 'hooray', $optimistic: true }, - ], - }; - } else { - return { kind: 'ProceedDefault' }; - } - }, - }), - { - wrapper, - } - ); - - act(() => mutationResult2.current.mutate({ data: { name: 'foo' } })); - - // cache should update - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].name).toBe('foohooray'); - }); - }); - - it('optimistic update mixed with non-zenstack queries', async () => { - const { queryClient, wrapper } = createWrapper(); - - // non-zenstack query - const { result: myQueryResult } = renderHook( - () => useQuery({ queryKey: ['myQuery'], queryFn: () => ({ data: 'myData' }) }), - { - wrapper, - } - ); - await waitFor(() => { - expect(myQueryResult.current.data).toEqual({ data: 'myData' }); - }); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, { - optimisticUpdate: true, - invalidateQueries: false, - }), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) - ); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].name).toBe('foo'); - }); - }); -}); diff --git a/packages/plugins/tanstack-query/tests/react-hooks.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks.test.tsx deleted file mode 100644 index 48ff6b650..000000000 --- a/packages/plugins/tanstack-query/tests/react-hooks.test.tsx +++ /dev/null @@ -1,781 +0,0 @@ -/** - * @jest-environment jsdom - */ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { act, renderHook, waitFor } from '@testing-library/react'; -import nock from 'nock'; -import React from 'react'; -import { getQueryKey } from '../src/runtime/common'; -import { RequestHandlerContext, useInfiniteModelQuery, useModelMutation, useModelQuery } from '../src/runtime/react'; -import { modelMeta } from './test-model-meta'; - -describe('Tanstack Query React Hooks V4 Test', () => { - function createWrapper() { - const queryClient = new QueryClient(); - const Provider = RequestHandlerContext.Provider; - const wrapper = ({ children }: { children: React.ReactElement }) => ( - - {/* @ts-ignore */} - {children} - - ); - return { queryClient, wrapper }; - } - - function makeUrl(model: string, operation: string, args?: unknown) { - let r = `http://localhost/api/model/${model}/${operation}`; - if (args) { - r += `?q=${encodeURIComponent(JSON.stringify(args))}`; - } - return r; - } - - beforeEach(() => { - nock.cleanAll(); - }); - - it('simple query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)).get(/.*/).reply(200, { - data, - }); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - expect(result.current.data).toMatchObject(data); - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject(data); - }); - }); - - it('infinite query', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Query findMany:', queryArgs); - return { - data: data, - }; - }); - - const { result } = renderHook( - () => - useInfiniteModelQuery('User', makeUrl('User', 'findMany'), queryArgs, { - getNextPageParam: () => null, - }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - const resultData = result.current.data!; - expect(resultData.pages).toHaveLength(1); - expect(resultData.pages[0]).toMatchObject(data); - expect(resultData?.pageParams).toHaveLength(1); - expect(resultData?.pageParams[0]).toBeUndefined(); - expect(result.current.hasNextPage).toBe(false); - const cacheData: any = queryClient.getQueryData( - getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }) - ); - expect(cacheData.pages[0]).toMatchObject(data); - }); - }); - - it('independent mutation and query', async () => { - const { wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - let queryCount = 0; - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - queryCount++; - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('Post', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - return { data: { id: '1', title: 'post1' } }; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('Post', 'POST', makeUrl('Post', 'create'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { title: 'post1' } })); - - await waitFor(() => { - // no refetch caused by invalidation - expect(queryCount).toBe(1); - }); - }); - - it('create and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findMany')), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '1', name: 'foo' }); - return { data: data[0] }; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(1); - }); - }); - - it('optimistic create single', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation( - 'User', - 'POST', - makeUrl('User', 'create'), - modelMeta, - { optimisticUpdate: true, invalidateQueries: false }, - undefined - ), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].name).toBe('foo'); - }); - }); - - it('optimistic create many', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'createMany')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation( - 'User', - 'POST', - makeUrl('User', 'createMany'), - modelMeta, - { optimisticUpdate: true, invalidateQueries: false }, - undefined - ), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: [{ name: 'foo' }, { name: 'bar' }] })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(2); - }); - }); - - it('update and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.name = 'bar'; - return data; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject({ name: 'bar' }); - }); - }); - - it('optimistic update', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return data; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation( - 'User', - 'PUT', - makeUrl('User', 'update'), - modelMeta, - { optimisticUpdate: true, invalidateQueries: false }, - undefined - ), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); - }); - }); - - it('optimistic upsert - create', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(0); - }); - - nock(makeUrl('User', 'upsert')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data: null }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation( - 'User', - 'POST', - makeUrl('User', 'upsert'), - modelMeta, - { optimisticUpdate: true, invalidateQueries: false }, - undefined - ), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ - where: { id: '1' }, - create: { id: '1', name: 'foo' }, - update: { name: 'bar' }, - }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(1); - expect(cacheData[0].$optimistic).toBe(true); - expect(cacheData[0].id).toBeTruthy(); - expect(cacheData[0].name).toBe('foo'); - }); - }); - - it('optimistic upsert - update', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' } }; - const data = { id: '1', name: 'foo' }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toMatchObject({ name: 'foo' }); - }); - - nock(makeUrl('User', 'upsert')) - .post(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return data; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation( - 'User', - 'POST', - makeUrl('User', 'upsert'), - modelMeta, - { optimisticUpdate: true, invalidateQueries: false }, - undefined - ), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ ...queryArgs, update: { name: 'bar' }, create: { name: 'zee' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); - }); - }); - - it('delete and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findMany')), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.splice(0, 1); - return { data: [] }; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'DELETE', makeUrl('User', 'delete'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(0); - }); - }); - - it('optimistic delete', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = [{ id: '1', name: 'foo' }]; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook( - () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), - { - wrapper, - } - ); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => { - console.log('Not mutating data'); - return { data }; - }); - - const { result: mutationResult } = renderHook( - () => - useModelMutation( - 'User', - 'DELETE', - makeUrl('User', 'delete'), - modelMeta, - { optimisticUpdate: true, invalidateQueries: false }, - undefined - ), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cacheData).toHaveLength(0); - }); - }); - - it('top-level mutation and nested-read invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' }, include: { posts: true } }; - const data = { posts: [{ id: '1', title: 'post1' }] }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('Post', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.posts[0].title = 'post2'; - return data; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('Post', 'PUT', makeUrl('Post', 'update'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { name: 'post2' } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData.posts[0].title).toBe('post2'); - }); - }); - - it('top-level mutation and nested-count invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const queryArgs = { where: { id: '1' }, include: { _count: { select: { posts: true } } } }; - const data = { _count: { posts: 1 } }; - - nock(makeUrl('User', 'findUnique', queryArgs)) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('Post', 'create')) - .post(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data._count.posts = 2; - return data; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('Post', 'POST', makeUrl('Post', 'create'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ data: { name: 'post2' } })); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); - expect(cacheData._count.posts).toBe(2); - }); - }); - - it('nested mutation and top-level-read invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data = [{ id: '1', title: 'post1', ownerId: '1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('Post', makeUrl('Post', 'findMany')), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toMatchObject(data); - }); - - nock(makeUrl('User', 'update')) - .put(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.push({ id: '2', title: 'post2', ownerId: '1' }); - return data; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta), - { - wrapper, - } - ); - - act(() => - mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { create: { title: 'post2' } } } }) - ); - - await waitFor(() => { - const cacheData: any = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); - expect(cacheData).toHaveLength(2); - }); - }); - - it('cascaded delete', async () => { - const { queryClient, wrapper } = createWrapper(); - - const data: any[] = [{ id: '1', title: 'post1', ownerId: '1' }]; - - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => { - console.log('Querying data:', JSON.stringify(data)); - return { data }; - }) - .persist(); - - const { result } = renderHook(() => useModelQuery('Post', makeUrl('Post', 'findMany')), { - wrapper, - }); - await waitFor(() => { - expect(result.current.data).toHaveLength(1); - }); - - nock(makeUrl('User', 'delete')) - .delete(/.*/) - .reply(200, () => { - console.log('Mutating data'); - data.pop(); - return { data: { id: '1' } }; - }); - - const { result: mutationResult } = renderHook( - () => useModelMutation('User', 'DELETE', makeUrl('User', 'delete'), modelMeta), - { - wrapper, - } - ); - - act(() => mutationResult.current.mutate({ where: { id: '1' } })); - - await waitFor(() => { - const cacheData = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); - expect(cacheData).toHaveLength(0); - }); - }); -}); diff --git a/packages/plugins/tanstack-query/tests/test-model-meta.ts b/packages/plugins/tanstack-query/tests/test-model-meta.ts deleted file mode 100644 index 6e08ce2ca..000000000 --- a/packages/plugins/tanstack-query/tests/test-model-meta.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { ModelMeta } from '@zenstackhq/runtime/cross'; - -const fieldDefaults = { - isId: false, - isDataModel: false, - isArray: false, - isOptional: true, - attributes: [], - isRelationOwner: false, - isForeignKey: false, -}; - -export const modelMeta: ModelMeta = { - models: { - user: { - name: 'user', - fields: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, - }, - name: { ...fieldDefaults, type: 'String', name: 'name' }, - email: { ...fieldDefaults, type: 'String', name: 'name', isOptional: false }, - posts: { - ...fieldDefaults, - type: 'Post', - isDataModel: true, - isArray: true, - name: 'posts', - }, - }, - uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, - }, - post: { - name: 'post', - fields: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, - }, - title: { ...fieldDefaults, type: 'String', name: 'title' }, - owner: { - ...fieldDefaults, - type: 'User', - name: 'owner', - isDataModel: true, - isRelationOwner: true, - foreignKeyMapping: { id: 'ownerId' }, - }, - ownerId: { ...fieldDefaults, type: 'String', name: 'ownerId', isForeignKey: true }, - category: { - ...fieldDefaults, - type: 'Category', - name: 'category', - isDataModel: true, - isRelationOwner: true, - backLink: 'posts', - foreignKeyMapping: { id: 'categoryId' }, - }, - categoryId: { - ...fieldDefaults, - type: 'String', - name: 'categoryId', - isForeignKey: true, - relationField: 'category', - }, - }, - uniqueConstraints: { id: { name: 'id', fields: ['id'] } }, - }, - category: { - name: 'category', - fields: { - id: { - ...fieldDefaults, - type: 'String', - isId: true, - name: 'id', - isOptional: false, - }, - name: { ...fieldDefaults, type: 'String', name: 'name' }, - posts: { - ...fieldDefaults, - type: 'Post', - isDataModel: true, - isArray: true, - name: 'posts', - }, - }, - }, - }, - deleteCascade: { - user: ['Post'], - }, -}; diff --git a/packages/plugins/tanstack-query/tsconfig.json b/packages/plugins/tanstack-query/tsconfig.json deleted file mode 100644 index c51ec9bae..000000000 --- a/packages/plugins/tanstack-query/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "lib": ["ESNext", "DOM"], - "outDir": "dist", - "jsx": "react" - }, - "include": ["src/**/*.ts"], - "exclude": ["src/runtime", "src/runtime-v5"] -} diff --git a/packages/plugins/tanstack-query/tsup-v5.config.ts b/packages/plugins/tanstack-query/tsup-v5.config.ts deleted file mode 100644 index 145f56a5c..000000000 --- a/packages/plugins/tanstack-query/tsup-v5.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/runtime-v5/index.ts', 'src/runtime-v5/react.ts', 'src/runtime-v5/vue.ts', 'src/runtime-v5/svelte.ts', 'src/runtime-v5/angular.ts'], - outDir: 'dist/runtime-v5', - splitting: false, - sourcemap: true, - clean: true, - dts: true, - format: ['cjs', 'esm'], -}); diff --git a/packages/plugins/tanstack-query/tsup.config.ts b/packages/plugins/tanstack-query/tsup.config.ts deleted file mode 100644 index 187d51aff..000000000 --- a/packages/plugins/tanstack-query/tsup.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/runtime/index.ts', 'src/runtime/react.ts', 'src/runtime/vue.ts', 'src/runtime/svelte.ts'], - outDir: 'dist/runtime', - splitting: false, - sourcemap: true, - clean: true, - dts: true, - format: ['cjs', 'esm'], -}); diff --git a/packages/plugins/trpc/CHANGELOG.md b/packages/plugins/trpc/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/plugins/trpc/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/plugins/trpc/LICENSE b/packages/plugins/trpc/LICENSE deleted file mode 120000 index 5853aaea5..000000000 --- a/packages/plugins/trpc/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../../LICENSE \ No newline at end of file diff --git a/packages/plugins/trpc/README.md b/packages/plugins/trpc/README.md deleted file mode 100644 index 2e41ff05e..000000000 --- a/packages/plugins/trpc/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ZenStack tRPC plugin - -This package contains ZenStack plugin for tRPC. The implementation is based on [prisma-trpc-generator](https://github.com/omar-dulaimi/prisma-trpc-generator). - -Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/plugins/trpc/jest.config.ts b/packages/plugins/trpc/jest.config.ts deleted file mode 120000 index 09c104090..000000000 --- a/packages/plugins/trpc/jest.config.ts +++ /dev/null @@ -1 +0,0 @@ -../../../jest.config.ts \ No newline at end of file diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json deleted file mode 100644 index 296e8b248..000000000 --- a/packages/plugins/trpc/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@zenstackhq/trpc", - "displayName": "ZenStack plugin for tRPC", - "version": "2.22.2", - "description": "ZenStack plugin for tRPC", - "main": "index.js", - "repository": { - "type": "git", - "url": "https://github.com/zenstackhq/zenstack" - }, - "scripts": { - "clean": "rimraf dist", - "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE \"res/**/*\" dist && pnpm pack dist --pack-destination ../../../../.build", - "watch": "tsc --watch", - "lint": "eslint src --ext ts", - "test": "jest", - "prepublishOnly": "pnpm build" - }, - "publishConfig": { - "directory": "dist", - "linkDirectory": true - }, - "keywords": [ - "trpc" - ], - "author": "ZenStack Team", - "license": "MIT", - "dependencies": { - "@zenstackhq/runtime": "workspace:*", - "@zenstackhq/sdk": "workspace:*", - "ts-morph": "catalog:", - "tslib": "^2.4.1" - }, - "peerDependencies": { - "zod": "catalog:" - }, - "devDependencies": { - "@trpc/next": "^10.32.0", - "@trpc/react-query": "^10.32.0", - "@trpc/server": "^10.32.0", - "@types/prettier": "^2.7.2", - "@types/tmp": "^0.2.3", - "@zenstackhq/testtools": "workspace:*", - "next": "14.2.4", - "tmp": "^0.2.3" - } -} diff --git a/packages/plugins/trpc/res/client/v10/next.ts b/packages/plugins/trpc/res/client/v10/next.ts deleted file mode 100644 index 6e7db4142..000000000 --- a/packages/plugins/trpc/res/client/v10/next.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable */ - -import type { AnyRouter } from '@trpc/server'; -import type { NextPageContext } from 'next'; -import { type CreateTRPCNext, createTRPCNext as _createTRPCNext } from '@trpc/next'; -import type { DeepOverrideAtPath } from './utils'; - -export function createTRPCNext< - TRouter extends AnyRouter, - TPath extends string | undefined = undefined, - TSSRContext extends NextPageContext = NextPageContext, - TFlags = null ->(opts: Parameters[0]) { - const r: CreateTRPCNext = _createTRPCNext(opts); - return r as DeepOverrideAtPath, ClientType, TPath>; -} diff --git a/packages/plugins/trpc/res/client/v10/nuxt.ts b/packages/plugins/trpc/res/client/v10/nuxt.ts deleted file mode 100644 index 5fe0591c8..000000000 --- a/packages/plugins/trpc/res/client/v10/nuxt.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable */ - -import type { AnyRouter } from '@trpc/server'; -import { createTRPCNuxtClient as _createTRPCNuxtClient } from 'trpc-nuxt/client'; -import type { DeepOverrideAtPath } from './utils'; - -export function createTRPCNuxtClient( - opts: Parameters>[0] -) { - const r = _createTRPCNuxtClient(opts); - return r as DeepOverrideAtPath, TPath>; -} diff --git a/packages/plugins/trpc/res/client/v10/react.ts b/packages/plugins/trpc/res/client/v10/react.ts deleted file mode 100644 index 5b204819c..000000000 --- a/packages/plugins/trpc/res/client/v10/react.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable */ - -import type { AnyRouter } from '@trpc/server'; -import type { CreateTRPCReactOptions } from '@trpc/react-query/shared'; -import { type CreateTRPCReact, createTRPCReact as _createTRPCReact } from '@trpc/react-query'; -import type { DeepOverrideAtPath } from './utils'; - -export function createTRPCReact< - TRouter extends AnyRouter, - TPath extends string | undefined = undefined, - TSSRContext = unknown, - TFlags = null ->(opts?: CreateTRPCReactOptions) { - const r: CreateTRPCReact = _createTRPCReact(opts); - return r as DeepOverrideAtPath, ClientType, TPath>; -} diff --git a/packages/plugins/trpc/res/client/v10/utils.ts b/packages/plugins/trpc/res/client/v10/utils.ts deleted file mode 100644 index 9b0daacdb..000000000 --- a/packages/plugins/trpc/res/client/v10/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable */ - -// inspired by: https://stackoverflow.com/questions/70632026/generic-to-recursively-modify-a-given-type-interface-in-typescript - -type Primitive = string | Function | number | boolean | Symbol | undefined | null; - -/** - * Recursively merges `T` and `R`. If there's a shared key, use `R`'s field type to overwrite `T`. - */ -export type DeepOverride = T extends Primitive - ? R - : R extends Primitive - ? R - : { - [K in keyof T]: K extends keyof R ? DeepOverride : T[K]; - } & { - [K in Exclude]: R[K]; - }; - -/** - * Traverse to `Path` (denoted by dot separated string literal type) in `T`, and starting from there, - * recursively merge with `R`. - */ -export type DeepOverrideAtPath = Path extends undefined - ? DeepOverride - : Path extends `${infer P1}.${infer P2}` - ? P1 extends keyof T - ? Omit & Record>> - : never - : Path extends keyof T - ? Omit & Record> - : never; - -// Utility type from 'trpc-nuxt' -export type KeysOf = Array; - -// Utility type from 'trpc-nuxt' -export type PickFrom> = T extends Array - ? T - : T extends Record - ? keyof T extends K[number] - ? T - : K[number] extends never - ? T - : Pick - : T; diff --git a/packages/plugins/trpc/res/client/v11/next.ts b/packages/plugins/trpc/res/client/v11/next.ts deleted file mode 100644 index edd0072b2..000000000 --- a/packages/plugins/trpc/res/client/v11/next.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable */ - -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; -import type { NextPageContext } from 'next'; -import { type CreateTRPCNext, createTRPCNext as _createTRPCNext } from '@trpc/next'; -import type { DeepOverrideAtPath } from './utils'; - -export function createTRPCNext< - TRouter extends AnyRouter, - TPath extends string | undefined = undefined, - TSSRContext extends NextPageContext = NextPageContext ->(opts: Parameters[0]) { - const r: CreateTRPCNext = _createTRPCNext(opts); - return r as DeepOverrideAtPath, ClientType, TPath>; -} diff --git a/packages/plugins/trpc/res/client/v11/nuxt.ts b/packages/plugins/trpc/res/client/v11/nuxt.ts deleted file mode 100644 index b107fdf0b..000000000 --- a/packages/plugins/trpc/res/client/v11/nuxt.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable */ - -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; -import { createTRPCNuxtClient as _createTRPCNuxtClient } from 'trpc-nuxt/client'; -import type { DeepOverrideAtPath } from './utils'; - -export function createTRPCNuxtClient( - opts: Parameters>[0] -) { - const r = _createTRPCNuxtClient(opts); - return r as DeepOverrideAtPath, TPath>; -} diff --git a/packages/plugins/trpc/res/client/v11/react.ts b/packages/plugins/trpc/res/client/v11/react.ts deleted file mode 100644 index 43a1f33eb..000000000 --- a/packages/plugins/trpc/res/client/v11/react.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable */ - -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; -import type { CreateTRPCReactOptions } from '@trpc/react-query/shared'; -import { type CreateTRPCReact, createTRPCReact as _createTRPCReact } from '@trpc/react-query'; -import type { DeepOverrideAtPath } from './utils'; - -export function createTRPCReact< - TRouter extends AnyRouter, - TPath extends string | undefined = undefined, - TSSRContext = unknown ->(opts?: CreateTRPCReactOptions) { - const r: CreateTRPCReact = _createTRPCReact(opts); - return r as DeepOverrideAtPath, ClientType, TPath>; -} diff --git a/packages/plugins/trpc/res/client/v11/utils.ts b/packages/plugins/trpc/res/client/v11/utils.ts deleted file mode 120000 index 45919501d..000000000 --- a/packages/plugins/trpc/res/client/v11/utils.ts +++ /dev/null @@ -1 +0,0 @@ -../v10/utils.ts \ No newline at end of file diff --git a/packages/plugins/trpc/src/client-helper/index.ts b/packages/plugins/trpc/src/client-helper/index.ts deleted file mode 100644 index a90f1338d..000000000 --- a/packages/plugins/trpc/src/client-helper/index.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { PluginError, type PluginOptions } from '@zenstackhq/sdk'; -import { getPrismaClientImportSpec } from '@zenstackhq/sdk/prisma'; -import { lowerCaseFirst, upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import fs from 'fs'; -import path from 'path'; -import { - InterfaceDeclarationStructure, - Project, - PropertySignatureStructure, - SourceFile, - StructureKind, -} from 'ts-morph'; -import { name } from '..'; -import { SupportedClientHelpers } from '../utils'; -import * as NextHelpers from './next'; -import * as NuxtHelpers from './nuxt'; -import * as ReactHelpers from './react'; - -const helpers = { - react: ReactHelpers, - next: NextHelpers, - nuxt: NuxtHelpers, -}; - -export function generateClientTypingForModel( - project: Project, - generateClientHelpers: SupportedClientHelpers[], - model: string, - options: PluginOptions, - generateOperations: Array<{ name: string; baseType: string }>, - version: string, - outDir: string -) { - for (const clientType of generateClientHelpers) { - const sf = project.createSourceFile( - path.resolve(outDir, `client/${upperCaseFirst(model)}.${clientType}.type.ts`), - undefined, - { - overwrite: true, - } - ); - - generateImports(clientType, sf, options, version); - - // generate a `ClientType` interface that contains typing for query/mutation operations - const routerTypingStructure: InterfaceDeclarationStructure = { - kind: StructureKind.Interface, - name: 'ClientType', - isExported: true, - typeParameters: ['AppRouter extends AnyRouter', `Context = AppRouter['_def']['_config']['$types']['ctx']`], - properties: [] as PropertySignatureStructure[], - }; - - for (const { name: generateOpName, baseType: baseOpType } of generateOperations) { - routerTypingStructure.properties?.push({ - kind: StructureKind.PropertySignature, - name: generateOpName, - type: (writer) => { - helpers[clientType].generateProcedureTyping(writer, generateOpName, model, baseOpType, version); - }, - }); - } - - sf.addInterface(routerTypingStructure); - } -} - -function generateImports( - clientHelperType: SupportedClientHelpers, - sourceFile: SourceFile, - options: PluginOptions, - version: string -) { - const importingDir = sourceFile.getDirectoryPath(); - const prismaImport = getPrismaClientImportSpec(importingDir, options); - sourceFile.addStatements([ - `import type { Prisma } from '${prismaImport}';`, - `import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client';`, - ]); - - // generate framework-specific imports - helpers[clientHelperType].generateRouterTypingImports(sourceFile, version); -} - -export function createClientHelperEntries( - project: Project, - outputDir: string, - generateClientHelpers: SupportedClientHelpers[], - models: string[], - version: string -) { - // generate utils - const content = fs.readFileSync(path.join(__dirname, `../res/client/${version}/utils.ts`), 'utf-8'); - project.createSourceFile(path.resolve(outputDir, 'client', `utils.ts`), content, { - overwrite: true, - }); - - for (const client of generateClientHelpers) { - createClientHelperEntryForType(project, client, models, version, outputDir); - } -} - -function createClientHelperEntryForType( - project: Project, - clientHelperType: SupportedClientHelpers, - models: string[], - version: string, - outputDir: string -) { - const content = fs.readFileSync(path.join(__dirname, `../res/client/${version}/${clientHelperType}.ts`), 'utf-8'); - const sf = project.createSourceFile(path.resolve(outputDir, 'client', `${clientHelperType}.ts`), content, { - overwrite: true, - }); - - sf.addInterface({ - name: 'ClientType', - typeParameters: ['AppRouter extends AnyRouter'], - isExported: true, - properties: models.map((model) => { - sf.addImportDeclaration({ - namedImports: [{ name: 'ClientType', alias: `${upperCaseFirst(model)}ClientType` }], - moduleSpecifier: `./${upperCaseFirst(model)}.${clientHelperType}.type`, - }); - return { - name: lowerCaseFirst(model), - type: `${upperCaseFirst(model)}ClientType`, - } as PropertySignatureStructure; - }), - }); -} - -/** - * Given a model and Prisma operation, returns related TS types. - */ -export function getPrismaOperationTypes(model: string, operation: string) { - // TODO: find a way to derive from Prisma Client API's generic types - // instead of duplicating them - - const capModel = upperCaseFirst(model); - const capOperation = upperCaseFirst(operation); - - let genericBase = `Prisma.${capModel}${capOperation}Args`; - const getPayload = `Prisma.${capModel}GetPayload`; - const selectSubset = `Prisma.SelectSubset`; - - let argsType: string; - let resultType: string; - const argsOptional = ['findMany', 'findFirst', 'findFirstOrThrow', 'createMany', 'deleteMany', 'count'].includes( - operation - ); - - switch (operation) { - case 'findUnique': - case 'findFirst': - argsType = selectSubset; - resultType = `${getPayload} | null`; - break; - - case 'findUniqueOrThrow': - case 'findFirstOrThrow': - argsType = selectSubset; - resultType = getPayload; - break; - - case 'findMany': - argsType = selectSubset; - resultType = `Array<${getPayload}>`; - break; - - case 'create': - argsType = selectSubset; - resultType = getPayload; - break; - - case 'createMany': - argsType = selectSubset; - resultType = `Prisma.BatchPayload`; - break; - - case 'update': - argsType = selectSubset; - resultType = getPayload; - break; - - case 'updateMany': - argsType = selectSubset; - resultType = `Prisma.BatchPayload`; - break; - - case 'upsert': - argsType = selectSubset; - resultType = getPayload; - break; - - case 'delete': - argsType = selectSubset; - resultType = getPayload; - break; - - case 'deleteMany': - argsType = selectSubset; - resultType = `Prisma.BatchPayload`; - break; - - case 'count': - argsType = `Prisma.Subset`; - resultType = `'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number`; - break; - - case 'aggregate': - argsType = `Prisma.Subset`; - resultType = `Prisma.Get${capModel}AggregateType`; - break; - - case 'groupBy': - genericBase = `Prisma.${capModel}GroupByArgs, - HasSelectOrTake extends Prisma.Or< - Prisma.Extends<'skip', Prisma.Keys>, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.${capModel}GroupByArgs['orderBy'] } - : { orderBy?: Prisma.${capModel}GroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? \`Error: "by" must not be empty.\` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? \`Error: Field "\${P}" used in "having" needs to be provided in "by".\` - : [ - Error, - 'Field ', - P, - \` in "having" needs to be provided in "by"\`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : \`Error: Field "\${P}" in "orderBy" needs to be provided in "by"\` - }[OrderFields] - `; - argsType = `Prisma.SubsetIntersection & InputErrors`; - resultType = `{} extends InputErrors ? Prisma.Get${capModel}GroupByPayload : InputErrors`; - break; - - default: - throw new PluginError(name, `Unsupported operation: "${operation}"`); - } - - return { genericBase, argsType, resultType, argsOptional }; -} diff --git a/packages/plugins/trpc/src/client-helper/next.ts b/packages/plugins/trpc/src/client-helper/next.ts deleted file mode 100644 index d71b66e76..000000000 --- a/packages/plugins/trpc/src/client-helper/next.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CodeBlockWriter, SourceFile } from 'ts-morph'; -import { - generateProcedureTyping as generateProcedureTypingForReact, - generateRouterTypingImports as generateRouterTypingImportsForReact, -} from './react'; - -export function generateRouterTypingImports(sourceFile: SourceFile, version: string) { - // next shares the same typing imports as react - generateRouterTypingImportsForReact(sourceFile, version); -} - -export function generateProcedureTyping( - writer: CodeBlockWriter, - opType: string, - modelName: string, - baseOpType: string, - version: string -) { - // next shares the same procedure typing as react - generateProcedureTypingForReact(writer, opType, modelName, baseOpType, version); -} diff --git a/packages/plugins/trpc/src/client-helper/nuxt.ts b/packages/plugins/trpc/src/client-helper/nuxt.ts deleted file mode 100644 index abd8a69c0..000000000 --- a/packages/plugins/trpc/src/client-helper/nuxt.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { CodeBlockWriter, SourceFile } from 'ts-morph'; -import { getProcedureTypeByOpName } from '../utils'; -import { getPrismaOperationTypes } from '.'; - -export function generateRouterTypingImports(sourceFile: SourceFile, version: string) { - sourceFile.addStatements([ - `import type { MaybeRefOrGetter, UnwrapRef } from 'vue';`, - `import type { AsyncData, AsyncDataOptions } from 'nuxt/app';`, - `import type { KeysOf, PickFrom } from './utils';`, - ]); - - if (version === 'v10') { - sourceFile.addStatements([`import type { AnyRouter } from '@trpc/server';`]); - } else { - sourceFile.addStatements([`import type { AnyTRPCRouter as AnyRouter } from '@trpc/server';`]); - } -} - -export function generateProcedureTyping( - writer: CodeBlockWriter, - opType: string, - modelName: string, - baseOpType: string, - _version: string -) { - const procType = getProcedureTypeByOpName(baseOpType); - const { genericBase, argsType, argsOptional, resultType } = getPrismaOperationTypes(modelName, opType); - const errorType = `TRPCClientErrorLike`; - const inputOptional = argsOptional ? '?' : ''; - - writer.block(() => { - if (procType === 'query') { - writer.writeLine(` - query: (input${inputOptional}: ${argsType}) => Promise<${resultType}>; - useQuery: = KeysOf, DefaultT = null>(input${inputOptional}: MaybeRefOrGetter<${argsType}>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: = KeysOf, DefaultT = null>(input${inputOptional}: MaybeRefOrGetter<${argsType}>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - `); - } else if (procType === 'mutation') { - writer.writeLine(` - mutate: (input${inputOptional}: ${argsType}) => Promise<${resultType}>; - useMutation: = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: = KeysOf>(input${inputOptional}: ${argsType}) => Promise | null, DataE>['data']>>; - }; - `); - } - }); -} diff --git a/packages/plugins/trpc/src/client-helper/react.ts b/packages/plugins/trpc/src/client-helper/react.ts deleted file mode 100644 index 9708ab24f..000000000 --- a/packages/plugins/trpc/src/client-helper/react.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { CodeBlockWriter, SourceFile } from 'ts-morph'; -import { getProcedureTypeByOpName } from '../utils'; -import { getPrismaOperationTypes } from '.'; - -export function generateRouterTypingImports(sourceFile: SourceFile, version: string) { - sourceFile.addStatements([ - `import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared';`, - ]); - if (version === 'v10') { - sourceFile.addStatements([`import type { AnyRouter } from '@trpc/server';`]); - } else { - sourceFile.addStatements([ - `import type { AnyTRPCRouter as AnyRouter } from '@trpc/server';`, - `import type { UseTRPCSuspenseQueryOptions, UseTRPCSuspenseQueryResult, UseTRPCSuspenseInfiniteQueryOptions, UseTRPCSuspenseInfiniteQueryResult } from '@trpc/react-query/shared';`, - ]); - } -} - -export function generateProcedureTyping( - writer: CodeBlockWriter, - opType: string, - modelName: string, - baseOpType: string, - version: string -) { - const procType = getProcedureTypeByOpName(baseOpType); - const { genericBase, argsType, argsOptional, resultType } = getPrismaOperationTypes(modelName, opType); - const errorType = `TRPCClientErrorLike`; - const inputOptional = argsOptional ? '?' : ''; - - writer.block(() => { - if (procType === 'query') { - if (version === 'v10') { - writer.writeLine(` - useQuery: ( - input${inputOptional}: ${argsType}, - opts?: UseTRPCQueryOptions - ) => UseTRPCQueryResult< - TData, - ${errorType} - >; - useInfiniteQuery: ( - input${inputOptional}: Omit<${argsType}, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions - ) => UseTRPCInfiniteQueryResult< - ${resultType}, - ${errorType} - >; - `); - } else { - writer.writeLine(` - useQuery: ( - input${inputOptional}: ${argsType}, - opts?: UseTRPCQueryOptions<${resultType}, TData, Error> - ) => UseTRPCQueryResult< - TData, - ${errorType} - >; - useInfiniteQuery: ( - input${inputOptional}: Omit<${argsType}, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions - ) => UseTRPCInfiniteQueryResult< - ${resultType}, - ${errorType}, - T - >; - useSuspenseQuery: ( - input${inputOptional}: ${argsType}, - opts?: UseTRPCSuspenseQueryOptions<${resultType}, TData, Error> - ) => UseTRPCSuspenseQueryResult; - useSuspenseInfiniteQuery: ( - input${inputOptional}: Omit<${argsType}, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions - ) => UseTRPCSuspenseInfiniteQueryResult<${resultType}, ${errorType}, T>; - `); - } - } else if (procType === 'mutation') { - writer.writeLine(` - useMutation: (opts?: UseTRPCMutationOptions< - ${genericBase}, - ${errorType}, - ${resultType}, - Context - >) => - Omit, 'mutateAsync'> & { - mutateAsync: - (variables${inputOptional}: T, opts?: UseTRPCMutationOptions) => Promise<${resultType}> - }; - `); - } - }); -} diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts deleted file mode 100644 index 66f067add..000000000 --- a/packages/plugins/trpc/src/generator.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { - CrudFailureReason, - ensureEmptyDir, - isDelegateModel, - parseOptionAsStrings, - PluginError, - requireOption, - resolvePath, - RUNTIME_PACKAGE, - saveProject, - type PluginOptions, -} from '@zenstackhq/sdk'; -import { DataModel, isDataModel, Model } from '@zenstackhq/sdk/ast'; -import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; -import { lowerCaseFirst, upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import path from 'path'; -import { Project } from 'ts-morph'; -import { name } from '.'; -import { createClientHelperEntries, generateClientTypingForModel } from './client-helper'; -import { project } from './project'; -import { - AllSupportedClientHelpers, - generateHelperImport, - generateProcedure, - generateRouterSchemaImport, - getInputSchemaByOpName, - resolveModelsComments, - SupportedClientHelpers, -} from './utils'; - -export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { - // resolve "generateModels" option - const generateModels = parseOptionAsStrings(options, 'generateModels', name); - - // resolve "generateModelActions" option - const generateModelActions = parseOptionAsStrings(options, 'generateModelActions', name); - - // resolve "generateClientHelpers" option - const generateClientHelpers = parseOptionAsStrings(options, 'generateClientHelpers', name); - if ( - generateClientHelpers && - !generateClientHelpers.every((v) => AllSupportedClientHelpers.includes(v as SupportedClientHelpers)) - ) { - throw new PluginError( - name, - `Option "generateClientHelpers" only supports the following values: ${AllSupportedClientHelpers.map( - (n) => '"' + n + '"' - ).join(', ')}.` - ); - } - - if (options.zodSchemasImport && typeof options.zodSchemasImport !== 'string') { - throw new PluginError(name, `Option "zodSchemasImport" must be a string`); - } - - let outDir = requireOption(options, 'output', name); - outDir = resolvePath(outDir, options); - ensureEmptyDir(outDir); - - const version = typeof options.version === 'string' ? options.version : 'v10'; - if (!['v10', 'v11'].includes(version)) { - throw new PluginError(name, `Unsupported tRPC version "${version}". Use "v10" (default) or "v11".`); - } - - if (version === 'v11') { - // v11 require options for importing `createTRPCRouter` and `procedure` - const importCreateRouter = options.importCreateRouter as string; - if (!importCreateRouter) { - throw new PluginError(name, `Option "importCreateRouter" is required for tRPC v11`); - } - - const importProcedure = options.importProcedure as string; - if (!importProcedure) { - throw new PluginError(name, `Option "importProcedure" is required for tRPC v11`); - } - } - - const prismaClientDmmf = dmmf; - - let modelOperations = prismaClientDmmf.mappings.modelOperations; - if (generateModels) { - modelOperations = modelOperations.filter((mo) => generateModels.includes(mo.model)); - } - - // TODO: remove this legacy code that deals with "@Gen.hide" comment syntax inherited - // from original code - const hiddenModels: string[] = []; - resolveModelsComments(prismaClientDmmf.datamodel.models, hiddenModels); - - const zodSchemasImport = (options.zodSchemasImport as string) ?? '@zenstackhq/runtime/zod'; - createAppRouter( - outDir, - modelOperations, - hiddenModels, - generateModelActions, - generateClientHelpers as SupportedClientHelpers[] | undefined, - model, - zodSchemasImport, - options, - version - ); - - createHelper(outDir); - - await saveProject(project); -} - -function createAppRouter( - outDir: string, - modelOperations: readonly DMMF.ModelMapping[], - hiddenModels: string[], - generateModelActions: string[] | undefined, - generateClientHelpers: SupportedClientHelpers[] | undefined, - zmodel: Model, - zodSchemasImport: string, - options: PluginOptions, - version: string -) { - const indexFile = path.resolve(outDir, 'routers', `index.ts`); - const appRouter = project.createSourceFile(indexFile, undefined, { - overwrite: true, - }); - - const prismaImport = getPrismaClientImportSpec(path.dirname(indexFile), options); - - if (version === 'v10') { - appRouter.addImportDeclarations([ - { - namedImports: [ - 'unsetMarker', - 'AnyRouter', - 'AnyRootConfig', - 'CreateRouterInner', - 'Procedure', - 'ProcedureBuilder', - 'ProcedureParams', - 'ProcedureRouterRecord', - 'ProcedureType', - ], - isTypeOnly: true, - moduleSpecifier: '@trpc/server', - }, - ]); - } else { - appRouter.addImportDeclarations([ - { - namedImports: ['AnyTRPCRouter as AnyRouter'], - isTypeOnly: true, - moduleSpecifier: '@trpc/server', - }, - ]); - } - - appRouter.addImportDeclarations([ - { - namedImports: ['PrismaClient'], - isTypeOnly: true, - moduleSpecifier: prismaImport, - }, - ]); - - if (version === 'v10') { - appRouter.addStatements(` - export type BaseConfig = AnyRootConfig; - - export type RouterFactory = < - ProcRouterRecord extends ProcedureRouterRecord - >( - procedures: ProcRouterRecord - ) => CreateRouterInner; - - export type UnsetMarker = typeof unsetMarker; - - export type ProcBuilder = ProcedureBuilder< - ProcedureParams - >; - `); - } else { - appRouter.addImportDeclaration({ - namedImports: ['createTRPCRouter'], - moduleSpecifier: options.importCreateRouter as string, - }); - } - - appRouter.addStatements(` - export function db(ctx: any) { - if (!ctx.prisma) { - throw new Error('Missing "prisma" field in trpc context'); - } - return ctx.prisma as PrismaClient; - } - `); - - const filteredModelOperations = modelOperations.filter((mo) => !hiddenModels.includes(mo.model)); - - appRouter - .addFunction({ - name: version === 'v10' ? 'createRouter' : 'createRouter', - parameters: - version === 'v10' - ? [ - { name: 'router', type: 'RouterFactory' }, - { name: 'procedure', type: 'ProcBuilder' }, - ] - : [], - isExported: true, - }) - .setBodyText((writer) => { - writer.write(`return ${version === 'v10' ? 'router' : 'createTRPCRouter'}(`); - writer.block(() => { - for (const modelOperation of filteredModelOperations) { - const { model, ...operations } = modelOperation; - - // "count" operation is missing from Prisma DMMF, add it here - operations.count = `count${model}`; - - generateModelCreateRouter( - project, - model, - operations, - outDir, - generateModelActions, - generateClientHelpers, - zodSchemasImport, - options, - zmodel, - version - ); - - appRouter.addImportDeclaration({ - defaultImport: `create${model}Router`, - moduleSpecifier: `./${model}.router`, - }); - - if (version === 'v10') { - writer.writeLine(`${lowerCaseFirst(model)}: create${model}Router(router, procedure),`); - } else { - writer.writeLine(`${lowerCaseFirst(model)}: create${model}Router(),`); - } - } - }); - writer.write(');'); - }); - - if (generateClientHelpers) { - createClientHelperEntries( - project, - outDir, - generateClientHelpers, - filteredModelOperations.map(({ model }) => model), - version - ); - } - - appRouter.formatText(); -} - -function generateModelCreateRouter( - project: Project, - model: string, - operations: Record, - outputDir: string, - generateModelActions: string[] | undefined, - generateClientHelpers: SupportedClientHelpers[] | undefined, - zodSchemasImport: string, - options: PluginOptions, - zmodel: Model, - version: string -) { - const modelRouter = project.createSourceFile(path.resolve(outputDir, 'routers', `${model}.router.ts`), undefined, { - overwrite: true, - }); - - if (version === 'v10') { - modelRouter.addImportDeclarations([ - { - namedImports: ['type RouterFactory', 'type ProcBuilder', 'type BaseConfig', 'db'], - moduleSpecifier: '.', - }, - ]); - } else { - modelRouter.addImportDeclarations([ - { - namedImports: ['db'], - moduleSpecifier: '.', - }, - ]); - - modelRouter.addImportDeclarations([ - { - namedImports: ['createTRPCRouter'], - moduleSpecifier: options.importCreateRouter as string, - }, - ]); - - modelRouter.addImportDeclarations([ - { - namedImports: ['procedure'], - moduleSpecifier: options.importProcedure as string, - }, - ]); - } - - // zod schema import - generateRouterSchemaImport(modelRouter, zodSchemasImport); - - // runtime helpers - generateHelperImport(modelRouter); - - const createRouterFunc = - version === 'v10' - ? modelRouter.addFunction({ - name: 'createRouter', - parameters: [ - { name: 'router', type: 'RouterFactory' }, - { name: 'procedure', type: 'ProcBuilder' }, - ], - isExported: true, - isDefaultExport: true, - }) - : modelRouter.addFunction({ - name: 'createRouter', - isExported: true, - isDefaultExport: true, - }); - - const dataModel = zmodel.declarations.find((d): d is DataModel => isDataModel(d) && d.name === model); - if (!dataModel) { - throw new Error(`Data model "${model}" not found`); - } - - const generateOperations: Array<{ name: string; baseType: string }> = []; - - createRouterFunc.setBodyText((funcWriter) => { - funcWriter.write(`return ${version === 'v10' ? 'router' : 'createTRPCRouter'}(`); - funcWriter.block(() => { - for (const [opType, opNameWithModel] of Object.entries(operations)) { - if (isDelegateModel(dataModel) && (opType.startsWith('create') || opType.startsWith('upsert'))) { - // delete models don't support create or upsert operations - continue; - } - - const baseOpType = opType.replace('OrThrow', ''); - const inputType = getInputSchemaByOpName(baseOpType, model); - const generateOpName = opType.replace(/One$/, ''); - - if ( - opNameWithModel && - inputType && - (!generateModelActions || generateModelActions.includes(generateOpName)) - ) { - if (generateOpName === 'createMany' && !supportCreateMany(zmodel)) { - continue; - } - - generateProcedure(funcWriter, generateOpName, upperCaseFirst(inputType), model, baseOpType); - generateOperations.push({ name: generateOpName, baseType: baseOpType }); - } - } - }); - funcWriter.write(');'); - }); - - if (generateClientHelpers) { - generateClientTypingForModel( - project, - generateClientHelpers, - model, - options, - generateOperations, - version, - outputDir - ); - } - - modelRouter.formatText(); -} - -function createHelper(outDir: string) { - const sf = project.createSourceFile(path.resolve(outDir, 'helper.ts'), undefined, { - overwrite: true, - }); - - sf.addStatements(`import { TRPCError } from '@trpc/server';`); - sf.addStatements(`import { isPrismaClientKnownRequestError } from '${RUNTIME_PACKAGE}';`); - - const checkMutate = sf.addFunction({ - name: 'checkMutate', - typeParameters: [{ name: 'T' }], - parameters: [ - { - name: 'promise', - type: 'Promise', - }, - ], - isAsync: true, - isExported: true, - returnType: 'Promise', - }); - - checkMutate.setBodyText( - `try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - if (err.meta?.reason === '${CrudFailureReason.RESULT_NOT_READABLE}') { - // unable to readback data - return undefined; - } else { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }); - } - } else { - throw err; - } - } - ` - ); - checkMutate.formatText(); - - const checkRead = sf.addFunction({ - name: 'checkRead', - typeParameters: [{ name: 'T' }], - parameters: [ - { - name: 'promise', - type: 'Promise', - }, - ], - isAsync: true, - isExported: true, - returnType: 'Promise', - }); - - checkRead.setBodyText( - `try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } else if (err.code === 'P2025') { - // not found - throw new TRPCError({ - code: 'NOT_FOUND', - message: err.message, - cause: err, - }); - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }) - } - } else { - throw err; - } - } - ` - ); - checkRead.formatText(); -} diff --git a/packages/plugins/trpc/src/index.ts b/packages/plugins/trpc/src/index.ts deleted file mode 100644 index 83125eb74..000000000 --- a/packages/plugins/trpc/src/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { PluginFunction } from '@zenstackhq/sdk'; -import { generate } from './generator'; - -export const name = 'tRPC'; -export const dependencies = ['@core/zod']; - -const run: PluginFunction = async (model, options, dmmf) => { - if (!dmmf) { - throw new Error('DMMF is required'); - } - return generate(model, options, dmmf); -}; - -export default run; diff --git a/packages/plugins/trpc/src/project.ts b/packages/plugins/trpc/src/project.ts deleted file mode 100644 index 2f1ea4c0f..000000000 --- a/packages/plugins/trpc/src/project.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Project, ScriptTarget, ModuleKind, CompilerOptions } from 'ts-morph'; - -const compilerOptions: CompilerOptions = { - target: ScriptTarget.ES2019, - module: ModuleKind.CommonJS, - emitDecoratorMetadata: true, - experimentalDecorators: true, - esModuleInterop: true, - skipLibCheck: true, -}; - -export const project = new Project({ - compilerOptions: { - ...compilerOptions, - }, -}); diff --git a/packages/plugins/trpc/src/utils.ts b/packages/plugins/trpc/src/utils.ts deleted file mode 100644 index 86d626bdc..000000000 --- a/packages/plugins/trpc/src/utils.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { type DMMF } from '@zenstackhq/sdk/prisma'; -import { lowerCaseFirst, upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { CodeBlockWriter, SourceFile } from 'ts-morph'; - -/** - * Supported client helper types - */ -export type SupportedClientHelpers = 'react' | 'next' | 'nuxt'; - -/** - * All supported client helper types - */ -export const AllSupportedClientHelpers: SupportedClientHelpers[] = ['react', 'next', 'nuxt']; - -export function generateProcedure( - writer: CodeBlockWriter, - opType: string, - typeName: string, - modelName: string, - baseOpType: string -) { - const procType = getProcedureTypeByOpName(baseOpType); - const prismaMethod = opType.replace('One', ''); - - if (procType === 'query') { - // the cast "as any" is to circumvent a TS compiler misfired error in certain cases - writer.write(` - ${opType}: procedure.input(${typeName}).query(({ctx, input}) => checkRead(db(ctx).${lowerCaseFirst( - modelName - )}.${prismaMethod}(input as any))), - `); - } else if (procType === 'mutation') { - // the cast "as any" is to circumvent a TS compiler misfired error in certain cases - writer.write(` - ${opType}: procedure.input(${typeName}).mutation(async ({ctx, input}) => checkMutate(db(ctx).${lowerCaseFirst( - modelName - )}.${prismaMethod}(input as any))), - `); - } -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function generateRouterSchemaImport(sourceFile: SourceFile, zodSchemasImport: string) { - sourceFile.addStatements([ - `import * as _Schema from '${zodSchemasImport}/input';`, - // temporary solution for dealing with the issue that Node.js wraps named exports under a `default` - // key when importing from a CJS module - `const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema;`, - ]); -} - -export function generateHelperImport(sourceFile: SourceFile) { - sourceFile.addStatements(`import { checkRead, checkMutate } from '../helper';`); -} - -export const getInputSchemaByOpName = (opName: string, modelName: string) => { - let inputType; - const capModelName = upperCaseFirst(modelName); - switch (opName) { - case 'findUnique': - inputType = `$Schema.${capModelName}InputSchema.findUnique`; - break; - case 'findFirst': - inputType = `$Schema.${capModelName}InputSchema.findFirst.optional()`; - break; - case 'findMany': - inputType = `$Schema.${capModelName}InputSchema.findMany.optional()`; - break; - case 'findRaw': - inputType = `$Schema.${capModelName}InputSchema.findRawObject`; - break; - case 'createOne': - inputType = `$Schema.${capModelName}InputSchema.create`; - break; - case 'createMany': - inputType = `$Schema.${capModelName}InputSchema.createMany.optional()`; - break; - case 'deleteOne': - inputType = `$Schema.${capModelName}InputSchema.delete`; - break; - case 'updateOne': - inputType = `$Schema.${capModelName}InputSchema.update`; - break; - case 'deleteMany': - inputType = `$Schema.${capModelName}InputSchema.deleteMany.optional()`; - break; - case 'updateMany': - inputType = `$Schema.${capModelName}InputSchema.updateMany`; - break; - case 'upsertOne': - inputType = `$Schema.${capModelName}InputSchema.upsert`; - break; - case 'aggregate': - inputType = `$Schema.${capModelName}InputSchema.aggregate`; - break; - case 'aggregateRaw': - inputType = `$Schema.${capModelName}InputSchema.aggregateRawObject`; - break; - case 'groupBy': - inputType = `$Schema.${capModelName}InputSchema.groupBy`; - break; - case 'count': - inputType = `$Schema.${capModelName}InputSchema.count.optional()`; - break; - default: - break; - } - return inputType; -}; - -export const getProcedureTypeByOpName = (opName: string) => { - let procType: string | undefined; - switch (opName) { - case 'findUnique': - case 'findFirst': - case 'findMany': - case 'findRaw': - case 'aggregate': - case 'aggregateRaw': - case 'groupBy': - case 'count': - procType = 'query'; - break; - case 'createOne': - case 'createMany': - case 'deleteOne': - case 'updateOne': - case 'deleteMany': - case 'updateMany': - case 'upsertOne': - procType = 'mutation'; - break; - default: - break; - } - return procType; -}; - -export function resolveModelsComments(models: readonly DMMF.Model[], hiddenModels: string[]) { - const modelAttributeRegex = /(@@Gen\.)+([A-z])+(\()+(.+)+(\))+/; - const attributeNameRegex = /(?:\.)+([A-Za-z])+(?:\()+/; - const attributeArgsRegex = /(?:\()+([A-Za-z])+:+(.+)+(?:\))+/; - - for (const model of models) { - if (model.documentation) { - const attribute = model.documentation?.match(modelAttributeRegex)?.[0]; - const attributeName = attribute?.match(attributeNameRegex)?.[0]?.slice(1, -1); - if (attributeName !== 'model') continue; - const rawAttributeArgs = attribute?.match(attributeArgsRegex)?.[0]?.slice(1, -1); - - const parsedAttributeArgs: Record = {}; - if (rawAttributeArgs) { - const rawAttributeArgsParts = rawAttributeArgs - .split(':') - .map((it) => it.trim()) - .map((part) => (part.startsWith('[') ? part : part.split(','))) - .flat() - .map((it) => it.trim()); - - for (let i = 0; i < rawAttributeArgsParts.length; i += 2) { - const key = rawAttributeArgsParts[i]; - const value = rawAttributeArgsParts[i + 1]; - parsedAttributeArgs[key] = JSON.parse(value); - } - } - if (parsedAttributeArgs.hide) { - hiddenModels.push(model.name); - } - } - } -} diff --git a/packages/plugins/trpc/tests/nuxt.test.ts b/packages/plugins/trpc/tests/nuxt.test.ts deleted file mode 100644 index 03471c5aa..000000000 --- a/packages/plugins/trpc/tests/nuxt.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -import { run } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('tRPC plugin tests with nuxt', () => { - let origDir: string | undefined; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - if (origDir) { - process.chdir(origDir); - } - }); - - it('project test trpc v10', () => { - const ver = require(path.join(__dirname, '../package.json')).version; - process.chdir(path.join(__dirname, './projects/nuxt-trpc-v10')); - - const deps = ['zenstackhq-language', 'zenstackhq-runtime', 'zenstackhq-sdk', 'zenstack']; - for (const dep of deps) { - run(`npm install ${path.join(__dirname, '../../../../.build/') + dep + '-' + ver + '.tgz'}`); - } - - run('npx zenstack generate'); - run('npm run build'); - }); - - it('project test trpc v11', () => { - const ver = require(path.join(__dirname, '../package.json')).version; - process.chdir(path.join(__dirname, './projects/nuxt-trpc-v11')); - - const deps = ['zenstackhq-language', 'zenstackhq-runtime', 'zenstackhq-sdk', 'zenstack']; - for (const dep of deps) { - run(`npm install ${path.join(__dirname, '../../../../.build/') + dep + '-' + ver + '.tgz'}`); - } - - run('npx zenstack generate'); - run('npm run build'); - }); -}); diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/.gitignore b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/.gitignore deleted file mode 100644 index cdb506446..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# Nuxt dev/build outputs -.output -.data -.nuxt -.nitro -.cache -dist - -# Node dependencies -node_modules - -# Logs -logs -*.log - -# Misc -.DS_Store -.fleet -.idea - -# Local env files -.env -.env.* -!.env.example - -*.db diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/README.md b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/README.md deleted file mode 100644 index f5db2a2db..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Nuxt 3 Minimal Starter - -Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. - -## Setup - -Make sure to install the dependencies: - -```bash -# npm -npm install - -# pnpm -pnpm install - -# yarn -yarn install - -# bun -bun install -``` - -## Development Server - -Start the development server on `http://localhost:3000`: - -```bash -# npm -npm run dev - -# pnpm -pnpm run dev - -# yarn -yarn dev - -# bun -bun run dev -``` - -## Production - -Build the application for production: - -```bash -# npm -npm run build - -# pnpm -pnpm run build - -# yarn -yarn build - -# bun -bun run build -``` - -Locally preview production build: - -```bash -# npm -npm run preview - -# pnpm -pnpm run preview - -# yarn -yarn preview - -# bun -bun run preview -``` - -Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/app.vue b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/app.vue deleted file mode 100644 index 01a7fa6f7..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/app.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/nuxt.config.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/nuxt.config.ts deleted file mode 100644 index 897b1221c..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/nuxt.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -// https://nuxt.com/docs/api/configuration/nuxt-config -export default defineNuxtConfig({ - compatibilityDate: '2024-04-03', - devtools: { enabled: true }, - build: { - transpile: ['trpc-nuxt'], - }, -}); diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package-lock.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package-lock.json deleted file mode 100644 index bc1ff51f5..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package-lock.json +++ /dev/null @@ -1,8892 +0,0 @@ -{ - "name": "nuxt-app", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nuxt-app", - "hasInstallScript": true, - "dependencies": { - "@prisma/client": "6.8.x", - "@trpc/client": "^10.45.2", - "@trpc/server": "^10.45.2", - "nuxt": "^3.14.1592", - "trpc-nuxt": "^0.10.22", - "vue": "latest", - "vue-router": "latest", - "zod": "^3.22.4" - }, - "devDependencies": { - "esbuild": "^0.24.0", - "prisma": "6.8.x", - "typescript": "^5.6.2", - "vue-tsc": "^2.1.10" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@antfu/utils": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", - "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", - "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", - "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", - "dependencies": { - "@babel/types": "^7.26.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.9.tgz", - "integrity": "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-decorators": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz", - "integrity": "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", - "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/standalone": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.26.2.tgz", - "integrity": "sha512-i2VbegsRfwa9yq3xmfDX3tG2yh9K0cCqwpSyVG2nPxifh0EOnucAZUeO/g4lW2Zfg03aPJNtPfxQbDHzXc7H+w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", - "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", - "dependencies": { - "mime": "^3.0.0" - }, - "engines": { - "node": ">=16.13" - } - }, - "node_modules/@cloudflare/kv-asset-handler/node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", - "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", - "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "dependencies": { - "debug": "^4.1.1" - } - }, - "node_modules/@kwsites/promise-deferred": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@netlify/functions": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.8.2.tgz", - "integrity": "sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==", - "dependencies": { - "@netlify/serverless-functions-api": "1.26.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@netlify/node-cookies": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@netlify/node-cookies/-/node-cookies-0.1.0.tgz", - "integrity": "sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g==", - "engines": { - "node": "^14.16.0 || >=16.0.0" - } - }, - "node_modules/@netlify/serverless-functions-api": { - "version": "1.26.1", - "resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.26.1.tgz", - "integrity": "sha512-q3L9i3HoNfz0SGpTIS4zTcKBbRkxzCRpd169eyiTuk3IwcPC3/85mzLHranlKo2b+HYT0gu37YxGB45aD8A3Tw==", - "dependencies": { - "@netlify/node-cookies": "^0.1.0", - "urlpattern-polyfill": "8.0.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nuxt/devalue": { - "version": "2.0.2", - "license": "MIT" - }, - "node_modules/@nuxt/devtools": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools/-/devtools-1.6.1.tgz", - "integrity": "sha512-s+4msaf8/REaXVbBDzjMgdUmEwR68hpoiQWx4QkH0JHSNQXWCWgNngqlZOM3DSRmPrelS57PJCag+L7gnT1wLw==", - "dependencies": { - "@antfu/utils": "^0.7.10", - "@nuxt/devtools-kit": "1.6.1", - "@nuxt/devtools-wizard": "1.6.1", - "@nuxt/kit": "^3.13.2", - "@vue/devtools-core": "7.6.4", - "@vue/devtools-kit": "7.6.4", - "birpc": "^0.2.17", - "consola": "^3.2.3", - "cronstrue": "^2.50.0", - "destr": "^2.0.3", - "error-stack-parser-es": "^0.1.5", - "execa": "^7.2.0", - "fast-npm-meta": "^0.2.2", - "flatted": "^3.3.1", - "get-port-please": "^3.1.2", - "hookable": "^5.5.3", - "image-meta": "^0.2.1", - "is-installed-globally": "^1.0.0", - "launch-editor": "^2.9.1", - "local-pkg": "^0.5.0", - "magicast": "^0.3.5", - "nypm": "^0.3.11", - "ohash": "^1.1.4", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.0", - "rc9": "^2.1.2", - "scule": "^1.3.0", - "semver": "^7.6.3", - "simple-git": "^3.27.0", - "sirv": "^2.0.4", - "tinyglobby": "^0.2.6", - "unimport": "^3.12.0", - "vite-plugin-inspect": "^0.8.7", - "vite-plugin-vue-inspector": "5.1.3", - "which": "^3.0.1", - "ws": "^8.18.0" - }, - "bin": { - "devtools": "cli.mjs" - }, - "peerDependencies": { - "vite": "*" - } - }, - "node_modules/@nuxt/devtools-kit": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-1.6.1.tgz", - "integrity": "sha512-6pvK5ih4XONVMSABlDbq6q7/TrZ++hyXGn5zdROVU780aYX3EjU8F0sq+1Lmc6ieiJg4tNe/EA+zV1onKRPsrQ==", - "dependencies": { - "@nuxt/kit": "^3.13.2", - "@nuxt/schema": "^3.13.2", - "execa": "^7.2.0" - }, - "peerDependencies": { - "vite": "*" - } - }, - "node_modules/@nuxt/devtools-wizard": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-1.6.1.tgz", - "integrity": "sha512-MpcKHgXJd4JyhJEvcIMTZqojyDFHLt9Wx2oWbV7YSEnubtHYxUM6p2M+Nb9/3mT+qoOiZQ+0db3xVcMW92oE8Q==", - "dependencies": { - "consola": "^3.2.3", - "diff": "^7.0.0", - "execa": "^7.2.0", - "global-directory": "^4.0.1", - "magicast": "^0.3.5", - "pathe": "^1.1.2", - "pkg-types": "^1.2.0", - "prompts": "^2.4.2", - "rc9": "^2.1.2", - "semver": "^7.6.3" - }, - "bin": { - "devtools-wizard": "cli.mjs" - } - }, - "node_modules/@nuxt/kit": { - "version": "3.14.1592", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.14.1592.tgz", - "integrity": "sha512-r9r8bISBBisvfcNgNL3dSIQHSBe0v5YkX5zwNblIC2T0CIEgxEVoM5rq9O5wqgb5OEydsHTtT2hL57vdv6VT2w==", - "dependencies": { - "@nuxt/schema": "3.14.1592", - "c12": "^2.0.1", - "consola": "^3.2.3", - "defu": "^6.1.4", - "destr": "^2.0.3", - "globby": "^14.0.2", - "hash-sum": "^2.0.0", - "ignore": "^6.0.2", - "jiti": "^2.4.0", - "klona": "^2.0.6", - "knitwork": "^1.1.0", - "mlly": "^1.7.3", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", - "scule": "^1.3.0", - "semver": "^7.6.3", - "ufo": "^1.5.4", - "unctx": "^2.3.1", - "unimport": "^3.13.2", - "untyped": "^1.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/@nuxt/kit/node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@nuxt/kit/node_modules/jiti": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", - "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/@nuxt/schema": { - "version": "3.14.1592", - "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.14.1592.tgz", - "integrity": "sha512-A1d/08ueX8stTXNkvGqnr1eEXZgvKn+vj6s7jXhZNWApUSqMgItU4VK28vrrdpKbjIPwq2SwhnGOHUYvN9HwCQ==", - "dependencies": { - "c12": "^2.0.1", - "compatx": "^0.1.8", - "consola": "^3.2.3", - "defu": "^6.1.4", - "hookable": "^5.5.3", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", - "scule": "^1.3.0", - "std-env": "^3.8.0", - "ufo": "^1.5.4", - "uncrypto": "^0.1.3", - "unimport": "^3.13.2", - "untyped": "^1.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/@nuxt/telemetry": { - "version": "2.6.0", - "license": "MIT", - "dependencies": { - "@nuxt/kit": "^3.13.1", - "ci-info": "^4.0.0", - "consola": "^3.2.3", - "create-require": "^1.1.1", - "defu": "^6.1.4", - "destr": "^2.0.3", - "dotenv": "^16.4.5", - "git-url-parse": "^15.0.0", - "is-docker": "^3.0.0", - "jiti": "^1.21.6", - "mri": "^1.2.0", - "nanoid": "^5.0.7", - "ofetch": "^1.3.4", - "package-manager-detector": "^0.2.0", - "parse-git-config": "^3.0.0", - "pathe": "^1.1.2", - "rc9": "^2.1.2", - "std-env": "^3.7.0" - }, - "bin": { - "nuxt-telemetry": "bin/nuxt-telemetry.mjs" - } - }, - "node_modules/@nuxt/vite-builder": { - "version": "3.14.1592", - "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-3.14.1592.tgz", - "integrity": "sha512-GVS7vkBJAGv13ghmjgGrS2QVyzoqxQ5+cAUrMeMjKbY7GnRY7/uOkoLmznYx8E/U9HBUyHQa+wSN2ZfcSiEytQ==", - "dependencies": { - "@nuxt/kit": "3.14.1592", - "@rollup/plugin-replace": "^6.0.1", - "@vitejs/plugin-vue": "^5.2.0", - "@vitejs/plugin-vue-jsx": "^4.1.0", - "autoprefixer": "^10.4.20", - "clear": "^0.1.0", - "consola": "^3.2.3", - "cssnano": "^7.0.6", - "defu": "^6.1.4", - "esbuild": "^0.24.0", - "escape-string-regexp": "^5.0.0", - "estree-walker": "^3.0.3", - "externality": "^1.0.2", - "get-port-please": "^3.1.2", - "h3": "^1.13.0", - "jiti": "^2.4.0", - "knitwork": "^1.1.0", - "magic-string": "^0.30.13", - "mlly": "^1.7.3", - "ohash": "^1.1.4", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.1", - "postcss": "^8.4.49", - "rollup-plugin-visualizer": "^5.12.0", - "std-env": "^3.8.0", - "strip-literal": "^2.1.0", - "ufo": "^1.5.4", - "unenv": "^1.10.0", - "unplugin": "^1.16.0", - "vite": "^5.4.11", - "vite-node": "^2.1.5", - "vite-plugin-checker": "^0.8.0", - "vue-bundle-renderer": "^2.1.1" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - }, - "peerDependencies": { - "vue": "^3.3.4" - } - }, - "node_modules/@nuxt/vite-builder/node_modules/jiti": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", - "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", - "hasInstallScript": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-wasm": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.5.0.tgz", - "integrity": "sha512-Z4ouuR8Pfggk1EYYbTaIoxc+Yv4o7cGQnH0Xy8+pQ+HbiW+ZnwhcD2LPf/prfq1nIWpAxjOkQ8uSMFWMtBLiVQ==", - "bundleDependencies": [ - "napi-wasm" - ], - "dependencies": { - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "napi-wasm": "^1.1.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" - }, - "node_modules/@prisma/client": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz", - "integrity": "sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==", - "hasInstallScript": true, - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@prisma/config": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.7.0.tgz", - "integrity": "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA==", - "devOptional": true, - "dependencies": { - "esbuild": ">=0.12 <1", - "esbuild-register": "3.6.0" - } - }, - "node_modules/@prisma/debug": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.7.0.tgz", - "integrity": "sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w==", - "devOptional": true - }, - "node_modules/@prisma/engines": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.7.0.tgz", - "integrity": "sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw==", - "devOptional": true, - "hasInstallScript": true, - "dependencies": { - "@prisma/debug": "6.7.0", - "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "@prisma/fetch-engine": "6.7.0", - "@prisma/get-platform": "6.7.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed.tgz", - "integrity": "sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA==", - "devOptional": true - }, - "node_modules/@prisma/fetch-engine": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.7.0.tgz", - "integrity": "sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ==", - "devOptional": true, - "dependencies": { - "@prisma/debug": "6.7.0", - "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "@prisma/get-platform": "6.7.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.7.0.tgz", - "integrity": "sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw==", - "devOptional": true, - "dependencies": { - "@prisma/debug": "6.7.0" - } - }, - "node_modules/@redocly/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js-replace": "^1.0.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@redocly/config": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.17.1.tgz", - "integrity": "sha512-CEmvaJuG7pm2ylQg53emPmtgm4nW2nxBgwXzbVEHpGas/lGnMyN8Zlkgiz6rPw0unASg6VW3wlz27SOL5XFHYQ==" - }, - "node_modules/@redocly/openapi-core": { - "version": "1.25.15", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.25.15.tgz", - "integrity": "sha512-/dpr5zpGj2t1Bf7EIXEboRZm1hsJZBQfv3Q1pkivtdAEg3if2khv+b9gY68aquC6cM/2aQY2kMLy8LlY2tn+Og==", - "dependencies": { - "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.17.0", - "colorette": "^1.2.0", - "https-proxy-agent": "^7.0.4", - "js-levenshtein": "^1.1.6", - "js-yaml": "^4.1.0", - "lodash.isequal": "^4.5.0", - "minimatch": "^5.0.1", - "node-fetch": "^2.6.1", - "pluralize": "^8.0.0", - "yaml-ast-parser": "0.0.43" - }, - "engines": { - "node": ">=14.19.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@redocly/openapi-core/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@rollup/plugin-alias": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", - "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", - "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rollup/plugin-inject": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", - "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-inject/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", - "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", - "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", - "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "license": "MIT" - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz", - "integrity": "sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz", - "integrity": "sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz", - "integrity": "sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz", - "integrity": "sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz", - "integrity": "sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz", - "integrity": "sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz", - "integrity": "sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz", - "integrity": "sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz", - "integrity": "sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz", - "integrity": "sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz", - "integrity": "sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz", - "integrity": "sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz", - "integrity": "sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz", - "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz", - "integrity": "sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz", - "integrity": "sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz", - "integrity": "sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz", - "integrity": "sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@trpc/client": { - "version": "10.45.2", - "funding": [ - "https://trpc.io/sponsor" - ], - "license": "MIT", - "peerDependencies": { - "@trpc/server": "10.45.2" - } - }, - "node_modules/@trpc/server": { - "version": "10.45.2", - "funding": [ - "https://trpc.io/sponsor" - ], - "license": "MIT" - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.15", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", - "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/node/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" - }, - "node_modules/@unhead/dom": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.13.tgz", - "integrity": "sha512-8Bpo3e50i49/z0TMiskQk3OqUVJpWOO0cnEEydJeFnjsPczDH76H3mWLvB11cv1B/rjLdBiPgui7yetFta5LCw==", - "dependencies": { - "@unhead/schema": "1.11.13", - "@unhead/shared": "1.11.13" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/@unhead/schema": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.13.tgz", - "integrity": "sha512-fIpQx6GCpl99l4qJXsPqkXxO7suMccuLADbhaMSkeXnVEi4ZIle+l+Ri0z+GHAEpJj17FMaQdO5n9FMSOMUxkw==", - "dependencies": { - "hookable": "^5.5.3", - "zhead": "^2.2.4" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/@unhead/shared": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.13.tgz", - "integrity": "sha512-EiJ3nsEtf6dvZ6OwVYrrrrCUl4ZE/9GTjpexEMti8EJXweSuL7SifNNXtIFk7UMoM0ULYxb7K/AKQV/odwoZyQ==", - "dependencies": { - "@unhead/schema": "1.11.13" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/@unhead/ssr": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/ssr/-/ssr-1.11.13.tgz", - "integrity": "sha512-LjomDIH8vXbnQQ8UVItmJ52BZBOyK12i1Q4W658X/f0VGtm0z3AulGQIvYla0rFcxAynDygfvWSC7xrlqDtRUw==", - "dependencies": { - "@unhead/schema": "1.11.13", - "@unhead/shared": "1.11.13" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/@unhead/vue": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.13.tgz", - "integrity": "sha512-s5++LqsNM01rkMQwtc4W19cP1fXC81o4YMyL+Kaqh9X0OPLeWnjONAh0U/Z2CIXBqhJHI+DoNXmDACXyuWPPxg==", - "dependencies": { - "@unhead/schema": "1.11.13", - "@unhead/shared": "1.11.13", - "defu": "^6.1.4", - "hookable": "^5.5.3", - "unhead": "1.11.13" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - }, - "peerDependencies": { - "vue": ">=2.7 || >=3" - } - }, - "node_modules/@vercel/nft": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.27.7.tgz", - "integrity": "sha512-FG6H5YkP4bdw9Ll1qhmbxuE8KwW2E/g8fJpM183fWQLeVDGqzeywMIeJ9h2txdWZ03psgWMn6QymTxaDLmdwUg==", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "@rollup/pluginutils": "^5.1.3", - "acorn": "^8.6.0", - "acorn-import-attributes": "^1.9.5", - "async-sema": "^3.1.1", - "bindings": "^1.4.0", - "estree-walker": "2.0.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.8", - "node-gyp-build": "^4.2.2", - "resolve-from": "^5.0.0" - }, - "bin": { - "nft": "out/cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@vercel/nft/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", - "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@vitejs/plugin-vue-jsx": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.1.1.tgz", - "integrity": "sha512-uMJqv/7u1zz/9NbWAD3XdjaY20tKTf17XVfQ9zq4wY1BjsB/PjpJPMe2xiG39QpP4ZdhYNhm4Hvo66uJrykNLA==", - "dependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-typescript": "^7.25.9", - "@vue/babel-plugin-jsx": "^1.2.5" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", - "vue": "^3.0.0" - } - }, - "node_modules/@volar/language-core": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz", - "integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==", - "devOptional": true, - "dependencies": { - "@volar/source-map": "2.4.10" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz", - "integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==", - "devOptional": true - }, - "node_modules/@volar/typescript": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz", - "integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==", - "devOptional": true, - "dependencies": { - "@volar/language-core": "2.4.10", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } - }, - "node_modules/@vue-macros/common": { - "version": "1.14.0", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.6", - "@rollup/pluginutils": "^5.1.0", - "@vue/compiler-sfc": "^3.5.4", - "ast-kit": "^1.1.0", - "local-pkg": "^0.5.0", - "magic-string-ast": "^0.6.2" - }, - "engines": { - "node": ">=16.14.0" - }, - "peerDependencies": { - "vue": "^2.7.0 || ^3.2.25" - }, - "peerDependenciesMeta": { - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/babel-helper-vue-transform-on": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.5.tgz", - "integrity": "sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==" - }, - "node_modules/@vue/babel-plugin-jsx": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.5.tgz", - "integrity": "sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.6", - "@babel/types": "^7.25.6", - "@vue/babel-helper-vue-transform-on": "1.2.5", - "@vue/babel-plugin-resolve-type": "1.2.5", - "html-tags": "^3.3.1", - "svg-tags": "^1.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - } - } - }, - "node_modules/@vue/babel-plugin-resolve-type": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.5.tgz", - "integrity": "sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/parser": "^7.25.6", - "@vue/compiler-sfc": "^3.5.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", - "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" - } - }, - "node_modules/@vue/compiler-core/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", - "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", - "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { - "version": "2.0.2", - "license": "MIT" - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", - "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" - } - }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", - "devOptional": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "license": "MIT" - }, - "node_modules/@vue/devtools-core": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.6.4.tgz", - "integrity": "sha512-blSwGVYpb7b5TALMjjoBiAl5imuBF7WEOAtaJaBMNikR8SQkm6mkUt4YlIKh9874/qoimwmpDOm+GHBZ4Y5m+g==", - "dependencies": { - "@vue/devtools-kit": "^7.6.4", - "@vue/devtools-shared": "^7.6.4", - "mitt": "^3.0.1", - "nanoid": "^3.3.4", - "pathe": "^1.1.2", - "vite-hot-client": "^0.2.3" - }, - "peerDependencies": { - "vue": "^3.0.0" - } - }, - "node_modules/@vue/devtools-core/node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/@vue/devtools-kit": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.4.tgz", - "integrity": "sha512-Zs86qIXXM9icU0PiGY09PQCle4TI750IPLmAJzW5Kf9n9t5HzSYf6Rz6fyzSwmfMPiR51SUKJh9sXVZu78h2QA==", - "dependencies": { - "@vue/devtools-shared": "^7.6.4", - "birpc": "^0.2.19", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.1" - } - }, - "node_modules/@vue/devtools-shared": { - "version": "7.6.7", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.7.tgz", - "integrity": "sha512-QggO6SviAsolrePAXZ/sA1dSicSPt4TueZibCvydfhNDieL1lAuyMTgQDGst7TEvMGb4vgYv2I+1sDkO4jWNnw==", - "dependencies": { - "rfdc": "^1.4.1" - } - }, - "node_modules/@vue/language-core": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz", - "integrity": "sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==", - "devOptional": true, - "dependencies": { - "@volar/language-core": "~2.4.8", - "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", - "@vue/shared": "^3.5.0", - "alien-signals": "^0.2.0", - "minimatch": "^9.0.3", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/language-core/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "devOptional": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", - "dependencies": { - "@vue/shared": "3.5.13" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", - "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", - "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", - "csstype": "^3.1.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", - "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" - }, - "peerDependencies": { - "vue": "3.5.13" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/alien-signals": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz", - "integrity": "sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==", - "devOptional": true - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "node_modules/archiver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", - "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", - "dependencies": { - "archiver-utils": "^5.0.2", - "async": "^3.2.4", - "buffer-crc32": "^1.0.0", - "readable-stream": "^4.0.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^6.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", - "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", - "dependencies": { - "glob": "^10.0.0", - "graceful-fs": "^4.2.0", - "is-stream": "^2.0.1", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/ast-kit": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.6", - "pathe": "^1.1.2" - }, - "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/ast-walker-scope": { - "version": "0.6.2", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.3", - "ast-kit": "^1.0.1" - }, - "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "node_modules/async-sema": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", - "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==" - }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/bare-events": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", - "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/birpc": { - "version": "0.2.19", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", - "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c12": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz", - "integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==", - "dependencies": { - "chokidar": "^4.0.1", - "confbox": "^0.1.7", - "defu": "^6.1.4", - "dotenv": "^16.4.5", - "giget": "^1.2.3", - "jiti": "^2.3.0", - "mlly": "^1.7.1", - "ohash": "^1.1.4", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.0", - "rc9": "^2.1.2" - }, - "peerDependencies": { - "magicast": "^0.3.5" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/c12/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/c12/node_modules/jiti": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", - "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/c12/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001685", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001685.tgz", - "integrity": "sha512-e/kJN1EMyHQzgcMEEgoo+YTCO1NGCmIYHk5Qk8jT6AazWemS5QFKJ5ShCJlH3GZrNIdZofcNCEwZqbMjjKzmnA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "4.0.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/citty": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/clear": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/clear/-/clear-0.1.0.tgz", - "integrity": "sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==", - "engines": { - "node": "*" - } - }, - "node_modules/clipboardy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz", - "integrity": "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==", - "dependencies": { - "execa": "^8.0.1", - "is-wsl": "^3.1.0", - "is64bit": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/clipboardy/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/clipboardy/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" - }, - "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" - }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "node_modules/compatx": { - "version": "0.1.8", - "license": "MIT" - }, - "node_modules/compress-commons": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", - "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/compress-commons/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==" - }, - "node_modules/consola": { - "version": "3.2.3", - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/cookie-es": { - "version": "1.2.2", - "license": "MIT" - }, - "node_modules/copy-anything": { - "version": "3.0.5", - "license": "MIT", - "dependencies": { - "is-what": "^4.1.8" - }, - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", - "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/croner": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/croner/-/croner-9.0.0.tgz", - "integrity": "sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA==", - "engines": { - "node": ">=18.0" - } - }, - "node_modules/cronstrue": { - "version": "2.52.0", - "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.52.0.tgz", - "integrity": "sha512-NKgHbWkSZXJUcaBHSsyzC8eegD6bBd4O0oCI6XMIJ+y4Bq3v4w7sY3wfWoKPuVlq9pQHRB6od0lmKpIqi8TlKA==", - "bin": { - "cronstrue": "bin/cli.js" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crossws": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.1.tgz", - "integrity": "sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.6.tgz", - "integrity": "sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==", - "dependencies": { - "cssnano-preset-default": "^7.0.6", - "lilconfig": "^3.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-preset-default": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.6.tgz", - "integrity": "sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==", - "dependencies": { - "browserslist": "^4.23.3", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.0", - "postcss-calc": "^10.0.2", - "postcss-colormin": "^7.0.2", - "postcss-convert-values": "^7.0.4", - "postcss-discard-comments": "^7.0.3", - "postcss-discard-duplicates": "^7.0.1", - "postcss-discard-empty": "^7.0.0", - "postcss-discard-overridden": "^7.0.0", - "postcss-merge-longhand": "^7.0.4", - "postcss-merge-rules": "^7.0.4", - "postcss-minify-font-values": "^7.0.0", - "postcss-minify-gradients": "^7.0.0", - "postcss-minify-params": "^7.0.2", - "postcss-minify-selectors": "^7.0.4", - "postcss-normalize-charset": "^7.0.0", - "postcss-normalize-display-values": "^7.0.0", - "postcss-normalize-positions": "^7.0.0", - "postcss-normalize-repeat-style": "^7.0.0", - "postcss-normalize-string": "^7.0.0", - "postcss-normalize-timing-functions": "^7.0.0", - "postcss-normalize-unicode": "^7.0.2", - "postcss-normalize-url": "^7.0.0", - "postcss-normalize-whitespace": "^7.0.0", - "postcss-ordered-values": "^7.0.1", - "postcss-reduce-initial": "^7.0.2", - "postcss-reduce-transforms": "^7.0.0", - "postcss-svgo": "^7.0.1", - "postcss-unique-selectors": "^7.0.3" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-utils": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", - "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/db0": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/db0/-/db0-0.2.1.tgz", - "integrity": "sha512-BWSFmLaCkfyqbSEZBQINMVNjCVfrogi7GQ2RSy1tmtfK9OXlsup6lUMwLsqSD7FbAjD04eWFdXowSHHUp6SE/Q==", - "peerDependencies": { - "@electric-sql/pglite": "*", - "@libsql/client": "*", - "better-sqlite3": "*", - "drizzle-orm": "*", - "mysql2": "*" - }, - "peerDependenciesMeta": { - "@electric-sql/pglite": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "drizzle-orm": { - "optional": true - }, - "mysql2": { - "optional": true - } - } - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "devOptional": true - }, - "node_modules/debug": { - "version": "4.3.7", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "engines": { - "node": ">=8" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "license": "MIT" - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destr": { - "version": "2.0.3", - "license": "MIT" - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==" - }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-prop": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", - "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", - "dependencies": { - "type-fest": "^4.18.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dotenv": { - "version": "16.4.5", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.68", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz", - "integrity": "sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-stack-parser-es": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", - "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/errx": { - "version": "0.1.0", - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" - }, - "node_modules/esbuild": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", - "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.0", - "@esbuild/android-arm": "0.24.0", - "@esbuild/android-arm64": "0.24.0", - "@esbuild/android-x64": "0.24.0", - "@esbuild/darwin-arm64": "0.24.0", - "@esbuild/darwin-x64": "0.24.0", - "@esbuild/freebsd-arm64": "0.24.0", - "@esbuild/freebsd-x64": "0.24.0", - "@esbuild/linux-arm": "0.24.0", - "@esbuild/linux-arm64": "0.24.0", - "@esbuild/linux-ia32": "0.24.0", - "@esbuild/linux-loong64": "0.24.0", - "@esbuild/linux-mips64el": "0.24.0", - "@esbuild/linux-ppc64": "0.24.0", - "@esbuild/linux-riscv64": "0.24.0", - "@esbuild/linux-s390x": "0.24.0", - "@esbuild/linux-x64": "0.24.0", - "@esbuild/netbsd-x64": "0.24.0", - "@esbuild/openbsd-arm64": "0.24.0", - "@esbuild/openbsd-x64": "0.24.0", - "@esbuild/sunos-x64": "0.24.0", - "@esbuild/win32-arm64": "0.24.0", - "@esbuild/win32-ia32": "0.24.0", - "@esbuild/win32-x64": "0.24.0" - } - }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", - "devOptional": true, - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/externality": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz", - "integrity": "sha512-LyExtJWKxtgVzmgtEHyQtLFpw1KFhQphF9nTG8TpAIVkiI/xQ3FJh75tRFLYl4hkn7BNIIdLJInuDAavX35pMw==", - "dependencies": { - "enhanced-resolve": "^5.14.1", - "mlly": "^1.3.0", - "pathe": "^1.1.1", - "ufo": "^1.1.2" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-npm-meta": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fast-npm-meta/-/fast-npm-meta-0.2.2.tgz", - "integrity": "sha512-E+fdxeaOQGo/CMWc9f4uHFfgUPJRAu7N3uB8GBvB3SDPAIWJK4GKyYhkAGFq+GYrcbKNfQIz5VVQyJnDuPPCrg==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/fastq": { - "version": "1.17.1", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==" - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-port-please": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", - "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==" - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/giget": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", - "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.2.3", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.3", - "nypm": "^0.3.8", - "ohash": "^1.1.3", - "pathe": "^1.1.2", - "tar": "^6.2.0" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, - "node_modules/git-config-path": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/git-up": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "is-ssh": "^1.4.0", - "parse-url": "^8.1.0" - } - }, - "node_modules/git-url-parse": { - "version": "15.0.0", - "license": "MIT", - "dependencies": { - "git-up": "^7.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/globby": { - "version": "14.0.2", - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, - "node_modules/gzip-size": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz", - "integrity": "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/h3": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.13.0.tgz", - "integrity": "sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==", - "dependencies": { - "cookie-es": "^1.2.2", - "crossws": ">=0.2.0 <0.4.0", - "defu": "^6.1.4", - "destr": "^2.0.3", - "iron-webcrypto": "^1.2.1", - "ohash": "^1.1.4", - "radix3": "^1.1.2", - "ufo": "^1.5.4", - "uncrypto": "^0.1.3", - "unenv": "^1.10.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, - "node_modules/hash-sum": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/hasown": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "devOptional": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hookable": { - "version": "5.5.3", - "license": "MIT" - }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-shutdown": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", - "integrity": "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/httpxy": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/httpxy/-/httpxy-0.1.5.tgz", - "integrity": "sha512-hqLDO+rfststuyEUTWObQK6zHEEmZ/kaIP2/zclGGZn6X8h/ESTWg+WKecQ/e5k4nPswjzZD+q2VqZIbr15CoQ==" - }, - "node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/image-meta": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.1.tgz", - "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==" - }, - "node_modules/impound": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/impound/-/impound-0.2.0.tgz", - "integrity": "sha512-gXgeSyp9Hf7qG2/PLKmywHXyQf2xFrw+mJGpoj9DsAB9L7/MIKn+DeEx98UryWXdmbv8wUUPdcQof6qXnZoCGg==", - "dependencies": { - "@rollup/pluginutils": "^5.1.2", - "mlly": "^1.7.2", - "pathe": "^1.1.2", - "unenv": "^1.10.0", - "unplugin": "^1.14.1" - } - }, - "node_modules/index-to-position": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz", - "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "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.", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ioredis": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", - "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/iron-webcrypto": { - "version": "1.2.1", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-installed-globally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", - "dependencies": { - "global-directory": "^4.0.1", - "is-path-inside": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "node_modules/is-number": { - "version": "7.0.0", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-ssh": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "protocols": "^2.0.1" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-what": { - "version": "4.1.16", - "license": "MIT", - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is64bit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", - "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", - "dependencies": { - "system-architecture": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "1.21.6", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/json5": { - "version": "2.2.3", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/knitwork": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" - }, - "node_modules/launch-editor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", - "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" - } - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/listhen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.9.0.tgz", - "integrity": "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==", - "dependencies": { - "@parcel/watcher": "^2.4.1", - "@parcel/watcher-wasm": "^2.4.1", - "citty": "^0.1.6", - "clipboardy": "^4.0.0", - "consola": "^3.2.3", - "crossws": ">=0.2.0 <0.4.0", - "defu": "^6.1.4", - "get-port-please": "^3.1.2", - "h3": "^1.12.0", - "http-shutdown": "^1.2.2", - "jiti": "^2.1.2", - "mlly": "^1.7.1", - "node-forge": "^1.3.1", - "pathe": "^1.1.2", - "std-env": "^3.7.0", - "ufo": "^1.5.4", - "untun": "^0.1.3", - "uqr": "^0.1.2" - }, - "bin": { - "listen": "bin/listhen.mjs", - "listhen": "bin/listhen.mjs" - } - }, - "node_modules/listhen/node_modules/jiti": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", - "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.14", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", - "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/magic-string-ast": { - "version": "0.6.2", - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.10" - }, - "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", - "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", - "funding": [ - "https://github.com/sponsors/broofa" - ], - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "5.1.6", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mlly": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", - "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", - "ufo": "^1.5.4" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "devOptional": true - }, - "node_modules/nanoid": { - "version": "5.0.7", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/nanotar": { - "version": "0.1.1", - "license": "MIT" - }, - "node_modules/nitropack": { - "version": "2.10.4", - "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.10.4.tgz", - "integrity": "sha512-sJiG/MIQlZCVSw2cQrFG1H6mLeSqHlYfFerRjLKz69vUfdu0EL2l0WdOxlQbzJr3mMv/l4cOlCCLzVRzjzzF/g==", - "dependencies": { - "@cloudflare/kv-asset-handler": "^0.3.4", - "@netlify/functions": "^2.8.2", - "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-inject": "^5.0.5", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-replace": "^6.0.1", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/pluginutils": "^5.1.3", - "@types/http-proxy": "^1.17.15", - "@vercel/nft": "^0.27.5", - "archiver": "^7.0.1", - "c12": "2.0.1", - "chokidar": "^3.6.0", - "citty": "^0.1.6", - "compatx": "^0.1.8", - "confbox": "^0.1.8", - "consola": "^3.2.3", - "cookie-es": "^1.2.2", - "croner": "^9.0.0", - "crossws": "^0.3.1", - "db0": "^0.2.1", - "defu": "^6.1.4", - "destr": "^2.0.3", - "dot-prop": "^9.0.0", - "esbuild": "^0.24.0", - "escape-string-regexp": "^5.0.0", - "etag": "^1.8.1", - "fs-extra": "^11.2.0", - "globby": "^14.0.2", - "gzip-size": "^7.0.0", - "h3": "^1.13.0", - "hookable": "^5.5.3", - "httpxy": "^0.1.5", - "ioredis": "^5.4.1", - "jiti": "^2.4.0", - "klona": "^2.0.6", - "knitwork": "^1.1.0", - "listhen": "^1.9.0", - "magic-string": "^0.30.12", - "magicast": "^0.3.5", - "mime": "^4.0.4", - "mlly": "^1.7.2", - "node-fetch-native": "^1.6.4", - "ofetch": "^1.4.1", - "ohash": "^1.1.4", - "openapi-typescript": "^7.4.2", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.1", - "pretty-bytes": "^6.1.1", - "radix3": "^1.1.2", - "rollup": "^4.24.3", - "rollup-plugin-visualizer": "^5.12.0", - "scule": "^1.3.0", - "semver": "^7.6.3", - "serve-placeholder": "^2.0.2", - "serve-static": "^1.16.2", - "std-env": "^3.7.0", - "ufo": "^1.5.4", - "uncrypto": "^0.1.3", - "unctx": "^2.3.1", - "unenv": "^1.10.0", - "unimport": "^3.13.1", - "unstorage": "^1.13.1", - "untyped": "^1.5.1", - "unwasm": "^0.3.9" - }, - "bin": { - "nitro": "dist/cli/index.mjs", - "nitropack": "dist/cli/index.mjs" - }, - "engines": { - "node": "^16.11.0 || >=17.0.0" - }, - "peerDependencies": { - "xml2js": "^0.6.2" - }, - "peerDependenciesMeta": { - "xml2js": { - "optional": true - } - } - }, - "node_modules/nitropack/node_modules/jiti": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", - "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.4", - "license": "MIT" - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nuxi": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/nuxi/-/nuxi-3.16.0.tgz", - "integrity": "sha512-t9m4zTq44R0/icuzQXJHEyPRM3YbgTPMpytyb6YW2LOL/3mwZ3Bmte1FIlCoigzDvxBJRbcchZGc689+Syyu8w==", - "bin": { - "nuxi": "bin/nuxi.mjs", - "nuxi-ng": "bin/nuxi.mjs", - "nuxt": "bin/nuxi.mjs", - "nuxt-cli": "bin/nuxi.mjs" - }, - "engines": { - "node": "^16.10.0 || >=18.0.0" - } - }, - "node_modules/nuxt": { - "version": "3.14.1592", - "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-3.14.1592.tgz", - "integrity": "sha512-roWAQH4Mb6WY72cNos+YVw0DgTCNAhNygiAMCedM7hbX6ESTR2n3VH7tU0yIWDPe/hfFdii4M4wWTTNHOtS44g==", - "dependencies": { - "@nuxt/devalue": "^2.0.2", - "@nuxt/devtools": "^1.6.0", - "@nuxt/kit": "3.14.1592", - "@nuxt/schema": "3.14.1592", - "@nuxt/telemetry": "^2.6.0", - "@nuxt/vite-builder": "3.14.1592", - "@unhead/dom": "^1.11.11", - "@unhead/shared": "^1.11.11", - "@unhead/ssr": "^1.11.11", - "@unhead/vue": "^1.11.11", - "@vue/shared": "^3.5.13", - "acorn": "8.14.0", - "c12": "^2.0.1", - "chokidar": "^4.0.1", - "compatx": "^0.1.8", - "consola": "^3.2.3", - "cookie-es": "^1.2.2", - "defu": "^6.1.4", - "destr": "^2.0.3", - "devalue": "^5.1.1", - "errx": "^0.1.0", - "esbuild": "^0.24.0", - "escape-string-regexp": "^5.0.0", - "estree-walker": "^3.0.3", - "globby": "^14.0.2", - "h3": "^1.13.0", - "hookable": "^5.5.3", - "ignore": "^6.0.2", - "impound": "^0.2.0", - "jiti": "^2.4.0", - "klona": "^2.0.6", - "knitwork": "^1.1.0", - "magic-string": "^0.30.13", - "mlly": "^1.7.3", - "nanotar": "^0.1.1", - "nitropack": "^2.10.4", - "nuxi": "^3.15.0", - "nypm": "^0.3.12", - "ofetch": "^1.4.1", - "ohash": "^1.1.4", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.1", - "radix3": "^1.1.2", - "scule": "^1.3.0", - "semver": "^7.6.3", - "std-env": "^3.8.0", - "strip-literal": "^2.1.0", - "tinyglobby": "0.2.10", - "ufo": "^1.5.4", - "ultrahtml": "^1.5.3", - "uncrypto": "^0.1.3", - "unctx": "^2.3.1", - "unenv": "^1.10.0", - "unhead": "^1.11.11", - "unimport": "^3.13.2", - "unplugin": "^1.16.0", - "unplugin-vue-router": "^0.10.8", - "unstorage": "^1.13.1", - "untyped": "^1.5.1", - "vue": "^3.5.13", - "vue-bundle-renderer": "^2.1.1", - "vue-devtools-stub": "^0.1.0", - "vue-router": "^4.4.5" - }, - "bin": { - "nuxi": "bin/nuxt.mjs", - "nuxt": "bin/nuxt.mjs" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - }, - "peerDependencies": { - "@parcel/watcher": "^2.1.0", - "@types/node": "^14.18.0 || >=16.10.0" - }, - "peerDependenciesMeta": { - "@parcel/watcher": { - "optional": true - }, - "@types/node": { - "optional": true - } - } - }, - "node_modules/nuxt/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/nuxt/node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/nuxt/node_modules/jiti": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", - "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/nuxt/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/nypm": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", - "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.2.3", - "execa": "^8.0.1", - "pathe": "^1.1.2", - "pkg-types": "^1.2.0", - "ufo": "^1.5.4" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, - "node_modules/nypm/node_modules/execa": { - "version": "8.0.1", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/nypm/node_modules/get-stream": { - "version": "8.0.1", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/human-signals": { - "version": "5.0.0", - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/nypm/node_modules/signal-exit": { - "version": "4.1.0", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ofetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", - "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", - "dependencies": { - "destr": "^2.0.3", - "node-fetch-native": "^1.6.4", - "ufo": "^1.5.4" - } - }, - "node_modules/ohash": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/openapi-typescript": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.4.4.tgz", - "integrity": "sha512-7j3nktnRzlQdlHnHsrcr6Gqz8f80/RhfA2I8s1clPI+jkY0hLNmnYVKBfuUEli5EEgK1B6M+ibdS5REasPlsUw==", - "dependencies": { - "@redocly/openapi-core": "^1.25.9", - "ansi-colors": "^4.1.3", - "parse-json": "^8.1.0", - "supports-color": "^9.4.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "openapi-typescript": "bin/cli.js" - }, - "peerDependencies": { - "typescript": "^5.x" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" - }, - "node_modules/package-manager-detector": { - "version": "0.2.0", - "license": "MIT" - }, - "node_modules/parse-git-config": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "git-config-path": "^2.0.0", - "ini": "^1.3.5" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parse-git-config/node_modules/ini": { - "version": "1.3.8", - "license": "ISC" - }, - "node_modules/parse-json": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", - "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "index-to-position": "^0.1.2", - "type-fest": "^4.7.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-path": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "protocols": "^2.0.0" - } - }, - "node_modules/parse-url": { - "version": "8.1.0", - "license": "MIT", - "dependencies": { - "parse-path": "^7.0.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "devOptional": true - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "node_modules/path-type": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pathe": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", - "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.2", - "pathe": "^1.1.2" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-calc": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.2.tgz", - "integrity": "sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==", - "dependencies": { - "postcss-selector-parser": "^6.1.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12 || ^20.9 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.38" - } - }, - "node_modules/postcss-colormin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", - "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-convert-values": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.4.tgz", - "integrity": "sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==", - "dependencies": { - "browserslist": "^4.23.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-comments": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz", - "integrity": "sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==", - "dependencies": { - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", - "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-empty": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", - "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", - "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.4.tgz", - "integrity": "sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^7.0.4" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-rules": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.4.tgz", - "integrity": "sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^5.0.0", - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", - "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", - "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-params": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", - "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", - "dependencies": { - "browserslist": "^4.23.3", - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.4.tgz", - "integrity": "sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==", - "dependencies": { - "cssesc": "^3.0.0", - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", - "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", - "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", - "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", - "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-string": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", - "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", - "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", - "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", - "dependencies": { - "browserslist": "^4.23.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", - "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", - "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-ordered-values": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", - "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", - "dependencies": { - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", - "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", - "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", - "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.3.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.3.tgz", - "integrity": "sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==", - "dependencies": { - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.7", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prisma": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.7.0.tgz", - "integrity": "sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw==", - "devOptional": true, - "hasInstallScript": true, - "dependencies": { - "@prisma/config": "6.7.0", - "@prisma/engines": "6.7.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=18.18" - }, - "optionalDependencies": { - "fsevents": "2.3.3" - }, - "peerDependencies": { - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/prompts": { - "version": "2.4.2", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/protocols": { - "version": "2.0.1", - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" - }, - "node_modules/radix3": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/rc9": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" - } - }, - "node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.0.tgz", - "integrity": "sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.0", - "@rollup/rollup-android-arm64": "4.28.0", - "@rollup/rollup-darwin-arm64": "4.28.0", - "@rollup/rollup-darwin-x64": "4.28.0", - "@rollup/rollup-freebsd-arm64": "4.28.0", - "@rollup/rollup-freebsd-x64": "4.28.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.0", - "@rollup/rollup-linux-arm-musleabihf": "4.28.0", - "@rollup/rollup-linux-arm64-gnu": "4.28.0", - "@rollup/rollup-linux-arm64-musl": "4.28.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.0", - "@rollup/rollup-linux-riscv64-gnu": "4.28.0", - "@rollup/rollup-linux-s390x-gnu": "4.28.0", - "@rollup/rollup-linux-x64-gnu": "4.28.0", - "@rollup/rollup-linux-x64-musl": "4.28.0", - "@rollup/rollup-win32-arm64-msvc": "4.28.0", - "@rollup/rollup-win32-ia32-msvc": "4.28.0", - "@rollup/rollup-win32-x64-msvc": "4.28.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-visualizer": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", - "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", - "dependencies": { - "open": "^8.4.0", - "picomatch": "^2.3.1", - "source-map": "^0.7.4", - "yargs": "^17.5.1" - }, - "bin": { - "rollup-plugin-visualizer": "dist/bin/cli.js" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "rollup": "2.x || 3.x || 4.x" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/scule": { - "version": "1.3.0", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.6.3", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-placeholder": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-2.0.2.tgz", - "integrity": "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==", - "dependencies": { - "defu": "^6.1.4" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "license": "ISC" - }, - "node_modules/simple-git": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", - "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", - "dependencies": { - "@kwsites/file-exists": "^1.1.1", - "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.5" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/git-js?sponsor=1" - } - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "license": "MIT" - }, - "node_modules/slash": { - "version": "5.1.0", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/smob": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==" - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/speakingurl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", - "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==" - }, - "node_modules/streamx": { - "version": "2.20.2", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.2.tgz", - "integrity": "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==", - "dependencies": { - "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==" - }, - "node_modules/stylehacks": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz", - "integrity": "sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==", - "dependencies": { - "browserslist": "^4.23.3", - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/superjson": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", - "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", - "dependencies": { - "copy-anything": "^3.0.2" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==" - }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/system-architecture": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", - "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/text-decoder": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", - "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==" - }, - "node_modules/tinyglobby": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", - "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", - "dependencies": { - "fdir": "^6.4.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/trpc-nuxt": { - "version": "0.10.22", - "resolved": "https://registry.npmjs.org/trpc-nuxt/-/trpc-nuxt-0.10.22.tgz", - "integrity": "sha512-krniLpq4FMHqhpQKMKJDBdYJ2NoCN51LOgOqOfaph0mZ3jo8czsgoFJFHRLLI6LFqahCdTmxbhOtELvXf1mCzw==", - "dependencies": { - "h3": "^1.11.1", - "ohash": "^1.1.3" - }, - "peerDependencies": { - "@trpc/client": "^10.38.1 <11", - "@trpc/server": "^10.38.1 <11", - "ofetch": "^1.3.3" - } - }, - "node_modules/type-fest": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz", - "integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.6.2", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.5.4", - "license": "MIT" - }, - "node_modules/ultrahtml": { - "version": "1.5.3", - "license": "MIT" - }, - "node_modules/uncrypto": { - "version": "0.1.3", - "license": "MIT" - }, - "node_modules/unctx": { - "version": "2.3.1", - "license": "MIT", - "dependencies": { - "acorn": "^8.8.2", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.0", - "unplugin": "^1.3.1" - } - }, - "node_modules/unenv": { - "version": "1.10.0", - "license": "MIT", - "dependencies": { - "consola": "^3.2.3", - "defu": "^6.1.4", - "mime": "^3.0.0", - "node-fetch-native": "^1.6.4", - "pathe": "^1.1.2" - } - }, - "node_modules/unenv/node_modules/mime": { - "version": "3.0.0", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/unhead": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.13.tgz", - "integrity": "sha512-I7yyvqRfpPPzXuCG7HKZkgAWJDbzXDDEVyib4C/78HREqhNGHVSyo4TqX1h1xB5cx7WYc21HHDRT2/8YkqOy2w==", - "dependencies": { - "@unhead/dom": "1.11.13", - "@unhead/schema": "1.11.13", - "@unhead/shared": "1.11.13", - "hookable": "^5.5.3" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unimport": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.2.tgz", - "integrity": "sha512-FSxhbAylGGanyuTb3K0Ka3T9mnsD0+cRKbwOS11Li4Lh2whWS091e32JH4bIHrTckxlW9GnExAglADlxXjjzFw==", - "dependencies": { - "@rollup/pluginutils": "^5.1.3", - "acorn": "^8.14.0", - "escape-string-regexp": "^5.0.0", - "estree-walker": "^3.0.3", - "local-pkg": "^0.5.1", - "magic-string": "^0.30.14", - "mlly": "^1.7.3", - "pathe": "^1.1.2", - "picomatch": "^4.0.2", - "pkg-types": "^1.2.1", - "scule": "^1.3.0", - "strip-literal": "^2.1.1", - "tinyglobby": "^0.2.10", - "unplugin": "^1.16.0" - } - }, - "node_modules/unimport/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unplugin": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.0.tgz", - "integrity": "sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==", - "dependencies": { - "acorn": "^8.14.0", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/unplugin-vue-router": { - "version": "0.10.8", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.4", - "@rollup/pluginutils": "^5.1.0", - "@vue-macros/common": "^1.12.2", - "ast-walker-scope": "^0.6.2", - "chokidar": "^3.6.0", - "fast-glob": "^3.3.2", - "json5": "^2.2.3", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.11", - "mlly": "^1.7.1", - "pathe": "^1.1.2", - "scule": "^1.3.0", - "unplugin": "^1.12.2", - "yaml": "^2.5.0" - }, - "peerDependencies": { - "vue-router": "^4.4.0" - }, - "peerDependenciesMeta": { - "vue-router": { - "optional": true - } - } - }, - "node_modules/unstorage": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.13.1.tgz", - "integrity": "sha512-ELexQHUrG05QVIM/iUeQNdl9FXDZhqLJ4yP59fnmn2jGUh0TEulwOgov1ubOb3Gt2ZGK/VMchJwPDNVEGWQpRg==", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^3.6.0", - "citty": "^0.1.6", - "destr": "^2.0.3", - "h3": "^1.13.0", - "listhen": "^1.9.0", - "lru-cache": "^10.4.3", - "node-fetch-native": "^1.6.4", - "ofetch": "^1.4.1", - "ufo": "^1.5.4" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.7.0", - "@azure/cosmos": "^4.1.1", - "@azure/data-tables": "^13.2.2", - "@azure/identity": "^4.5.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.25.0", - "@capacitor/preferences": "^6.0.2", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/kv": "^1.0.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.1" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - } - } - }, - "node_modules/unstorage/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "node_modules/untun": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz", - "integrity": "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==", - "dependencies": { - "citty": "^0.1.5", - "consola": "^3.2.3", - "pathe": "^1.1.1" - }, - "bin": { - "untun": "bin/untun.mjs" - } - }, - "node_modules/untyped": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.1.tgz", - "integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==", - "dependencies": { - "@babel/core": "^7.25.7", - "@babel/standalone": "^7.25.7", - "@babel/types": "^7.25.7", - "defu": "^6.1.4", - "jiti": "^2.3.1", - "mri": "^1.2.0", - "scule": "^1.3.0" - }, - "bin": { - "untyped": "dist/cli.mjs" - } - }, - "node_modules/untyped/node_modules/jiti": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", - "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/unwasm": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/unwasm/-/unwasm-0.3.9.tgz", - "integrity": "sha512-LDxTx/2DkFURUd+BU1vUsF/moj0JsoTvl+2tcg2AUOiEzVturhGGx17/IMgGvKUYdZwr33EJHtChCJuhu9Ouvg==", - "dependencies": { - "knitwork": "^1.0.0", - "magic-string": "^0.30.8", - "mlly": "^1.6.1", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "unplugin": "^1.10.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uqr": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", - "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==" - }, - "node_modules/uri-js-replace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", - "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==" - }, - "node_modules/urlpattern-polyfill": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", - "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-hot-client": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-0.2.4.tgz", - "integrity": "sha512-a1nzURqO7DDmnXqabFOliz908FRmIppkBKsJthS8rbe8hBEXwEwe4C3Pp33Z1JoFCYfVL4kTOMLKk0ZZxREIeA==", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" - } - }, - "node_modules/vite-node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", - "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-plugin-checker": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz", - "integrity": "sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "ansi-escapes": "^4.3.0", - "chalk": "^4.1.1", - "chokidar": "^3.5.1", - "commander": "^8.0.0", - "fast-glob": "^3.2.7", - "fs-extra": "^11.1.0", - "npm-run-path": "^4.0.1", - "strip-ansi": "^6.0.0", - "vscode-languageclient": "^7.0.0", - "vscode-languageserver": "^7.0.0", - "vscode-languageserver-textdocument": "^1.0.1", - "vscode-uri": "^3.0.2" - }, - "engines": { - "node": ">=14.16" - }, - "peerDependencies": { - "@biomejs/biome": ">=1.7", - "eslint": ">=7", - "meow": "^9.0.0", - "optionator": "^0.9.1", - "stylelint": ">=13", - "typescript": "*", - "vite": ">=2.0.0", - "vls": "*", - "vti": "*", - "vue-tsc": "~2.1.6" - }, - "peerDependenciesMeta": { - "@biomejs/biome": { - "optional": true - }, - "eslint": { - "optional": true - }, - "meow": { - "optional": true - }, - "optionator": { - "optional": true - }, - "stylelint": { - "optional": true - }, - "typescript": { - "optional": true - }, - "vls": { - "optional": true - }, - "vti": { - "optional": true - }, - "vue-tsc": { - "optional": true - } - } - }, - "node_modules/vite-plugin-checker/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/vite-plugin-checker/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/vite-plugin-inspect": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.8.tgz", - "integrity": "sha512-aZlBuXsWUPJFmMK92GIv6lH7LrwG2POu4KJ+aEdcqnu92OAf+rhBnfMDQvxIJPEB7hE2t5EyY/PMgf5aDLT8EA==", - "dependencies": { - "@antfu/utils": "^0.7.10", - "@rollup/pluginutils": "^5.1.3", - "debug": "^4.3.7", - "error-stack-parser-es": "^0.1.5", - "fs-extra": "^11.2.0", - "open": "^10.1.0", - "perfect-debounce": "^1.0.0", - "picocolors": "^1.1.1", - "sirv": "^3.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/vite-plugin-inspect/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vite-plugin-inspect/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vite-plugin-inspect/node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite-plugin-vue-inspector": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.1.3.tgz", - "integrity": "sha512-pMrseXIDP1Gb38mOevY+BvtNGNqiqmqa2pKB99lnLsADQww9w9xMbAfT4GB6RUoaOkSPrtlXqpq2Fq+Dj2AgFg==", - "dependencies": { - "@babel/core": "^7.23.0", - "@babel/plugin-proposal-decorators": "^7.23.0", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-transform-typescript": "^7.22.15", - "@vue/babel-plugin-jsx": "^1.1.5", - "@vue/compiler-dom": "^3.3.4", - "kolorist": "^1.8.0", - "magic-string": "^0.30.4" - }, - "peerDependencies": { - "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0" - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", - "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", - "engines": { - "node": ">=8.0.0 || >=10.0.0" - } - }, - "node_modules/vscode-languageclient": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", - "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", - "dependencies": { - "minimatch": "^3.0.4", - "semver": "^7.3.4", - "vscode-languageserver-protocol": "3.16.0" - }, - "engines": { - "vscode": "^1.52.0" - } - }, - "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/vscode-languageserver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", - "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", - "dependencies": { - "vscode-languageserver-protocol": "3.16.0" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", - "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", - "dependencies": { - "vscode-jsonrpc": "6.0.0", - "vscode-languageserver-types": "3.16.0" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", - "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" - }, - "node_modules/vscode-uri": { - "version": "3.0.8", - "license": "MIT" - }, - "node_modules/vue": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", - "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-bundle-renderer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.1.tgz", - "integrity": "sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==", - "dependencies": { - "ufo": "^1.5.4" - } - }, - "node_modules/vue-devtools-stub": { - "version": "0.1.0", - "license": "MIT" - }, - "node_modules/vue-router": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz", - "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/vue-tsc": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz", - "integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==", - "devOptional": true, - "dependencies": { - "@volar/typescript": "~2.4.8", - "@vue/language-core": "2.1.10", - "semver": "^7.5.4" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/yaml": { - "version": "2.5.1", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yaml-ast-parser": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/zhead": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", - "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/zip-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", - "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", - "dependencies": { - "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.2", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/zod": { - "version": "3.23.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json deleted file mode 100644 index 40ebf8802..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "nuxt-app", - "private": true, - "type": "module", - "scripts": { - "build": "nuxt typecheck && nuxt build", - "dev": "nuxt dev", - "generate": "nuxt generate", - "preview": "nuxt preview", - "postinstall": "nuxt prepare" - }, - "dependencies": { - "@prisma/client": "6.19.x", - "@trpc/client": "^10.45.2", - "@trpc/server": "^10.45.2", - "nuxt": "^3.14.1592", - "trpc-nuxt": "^0.10.22", - "vue": "latest", - "vue-router": "latest", - "zod": "^3.25.0" - }, - "devDependencies": { - "esbuild": "^0.24.0", - "prisma": "6.19.x", - "typescript": "^5.6.2", - "vue-tsc": "^2.1.10" - } -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/plugins/client.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/plugins/client.ts deleted file mode 100644 index 9493be346..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/plugins/client.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { httpBatchLink } from 'trpc-nuxt/client'; -import type { AppRouter } from '~/server/trpc/routers'; -import { createTRPCNuxtClient } from '~/server/trpc/routers/generated/client/nuxt'; - -export default defineNuxtPlugin(() => { - /** - * createTRPCNuxtClient adds a `useQuery` composable - * built on top of `useAsyncData`. - */ - const client = createTRPCNuxtClient({ - links: [ - httpBatchLink({ - url: '/api/trpc', - }), - ], - }); - - return { - provide: { - client, - }, - }; -}); diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/prisma/schema.prisma b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/prisma/schema.prisma deleted file mode 100644 index 71cd1ce9b..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/prisma/schema.prisma +++ /dev/null @@ -1,31 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -////////////////////////////////////////////////////////////////////////////////////////////// - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -generator client { - provider = "prisma-client-js" -} - -model User { - id String @id() @default(cuid()) - email String @unique() - password String - posts Post[] -} - -model Post { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - title String - content String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/public/favicon.ico b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/public/favicon.ico deleted file mode 100644 index 18993ad91cfd43e03b074dd0b5cc3f37ab38e49c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmeHLOKuuL5PjK%MHWVi6lD zOGiREbCw`xmFozJ^aNatJY>w+g ze6a2@u~m#^BZm@8wco9#Crlli0uLb^3E$t2-WIc^#(?t)*@`UpuofJ(Uyh@F>b3Ph z$D^m8Xq~pTkGJ4Q`Q2)te3mgkWYZ^Ijq|hkiP^9`De={bQQ%heZC$QU2UpP(-tbl8 zPWD2abEew;oat@w`uP3J^YpsgT%~jT(Dk%oU}sa$7|n6hBjDj`+I;RX(>)%lm_7N{+B7Mu%H?422lE%MBJH!!YTN2oT7xr>>N-8OF$C&qU^ z>vLsa{$0X%q1fjOe3P1mCv#lN{xQ4_*HCSAZjTb1`}mlc+9rl8$B3OP%VT@mch_~G z7Y+4b{r>9e=M+7vSI;BgB?ryZDY4m>&wcHSn81VH1N~`0gvwH{ z8dv#hG|OK`>1;j7tM#B)Z7zDN?{6=dUal}$e ({ - prisma, -}); - -export type Context = inferAsyncReturnType; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/Post.nuxt.type.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/Post.nuxt.type.ts deleted file mode 100644 index ff30d295e..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/Post.nuxt.type.ts +++ /dev/null @@ -1,385 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { Prisma } from '@prisma/client'; -import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; -import type { MaybeRefOrGetter, UnwrapRef } from 'vue'; -import type { AsyncData, AsyncDataOptions } from 'nuxt/app'; -import type { KeysOf, PickFrom } from './utils'; -import type { AnyRouter } from '@trpc/server'; - -export interface ClientType { - aggregate: { - - query: (input: Prisma.Subset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - createMany: { - - mutate: (input?: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input?: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - create: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - deleteMany: { - - mutate: (input?: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input?: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - delete: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - findFirst: { - - query: (input?: Prisma.SelectSubset) => Promise | null>; - useQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findFirstOrThrow: { - - query: (input?: Prisma.SelectSubset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findMany: { - - query: (input?: Prisma.SelectSubset) => Promise>>; - useQuery: >, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: >, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findUnique: { - - query: (input: Prisma.SelectSubset) => Promise | null>; - useQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findUniqueOrThrow: { - - query: (input: Prisma.SelectSubset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - groupBy: { - - query: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >(input: Prisma.SubsetIntersection & InputErrors) => Promise<{} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors>; - useQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , ResT = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter & InputErrors>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , ResT = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter & InputErrors>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - updateMany: { - - mutate: (input: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - update: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - upsert: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - count: { - - query: (input?: Prisma.Subset) => Promise<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number>; - useQuery: - : number, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: - : number, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/User.nuxt.type.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/User.nuxt.type.ts deleted file mode 100644 index d1d7fe182..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/User.nuxt.type.ts +++ /dev/null @@ -1,385 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { Prisma } from '@prisma/client'; -import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; -import type { MaybeRefOrGetter, UnwrapRef } from 'vue'; -import type { AsyncData, AsyncDataOptions } from 'nuxt/app'; -import type { KeysOf, PickFrom } from './utils'; -import type { AnyRouter } from '@trpc/server'; - -export interface ClientType { - aggregate: { - - query: (input: Prisma.Subset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - createMany: { - - mutate: (input?: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input?: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - create: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - deleteMany: { - - mutate: (input?: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input?: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - delete: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - findFirst: { - - query: (input?: Prisma.SelectSubset) => Promise | null>; - useQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findFirstOrThrow: { - - query: (input?: Prisma.SelectSubset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findMany: { - - query: (input?: Prisma.SelectSubset) => Promise>>; - useQuery: >, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: >, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findUnique: { - - query: (input: Prisma.SelectSubset) => Promise | null>; - useQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findUniqueOrThrow: { - - query: (input: Prisma.SelectSubset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - groupBy: { - - query: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >(input: Prisma.SubsetIntersection & InputErrors) => Promise<{} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors>; - useQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , ResT = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter & InputErrors>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , ResT = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter & InputErrors>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - updateMany: { - - mutate: (input: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - update: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - upsert: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - count: { - - query: (input?: Prisma.Subset) => Promise<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number>; - useQuery: - : number, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: - : number, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/nuxt.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/nuxt.ts deleted file mode 100644 index 43a9d0666..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/nuxt.ts +++ /dev/null @@ -1,23 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { AnyRouter } from '@trpc/server'; -import { createTRPCNuxtClient as _createTRPCNuxtClient } from 'trpc-nuxt/client'; -import type { DeepOverrideAtPath } from './utils'; -import { ClientType as UserClientType } from "./User.nuxt.type"; -import { ClientType as PostClientType } from "./Post.nuxt.type"; - -export function createTRPCNuxtClient( - opts: Parameters>[0] -) { - const r = _createTRPCNuxtClient(opts); - return r as DeepOverrideAtPath, TPath>; -} - -export interface ClientType { - user: UserClientType; - post: PostClientType; -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/utils.ts deleted file mode 100644 index 830996b8c..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/client/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -type Primitive = string | Function | number | boolean | Symbol | undefined | null; - -/** - * Recursively merges `T` and `R`. If there's a shared key, use `R`'s field type to overwrite `T`. - */ -export type DeepOverride = T extends Primitive - ? R - : R extends Primitive - ? R - : { - [K in keyof T]: K extends keyof R ? DeepOverride : T[K]; - } & { - [K in Exclude]: R[K]; - }; - -/** - * Traverse to `Path` (denoted by dot separated string literal type) in `T`, and starting from there, - * recursively merge with `R`. - */ -export type DeepOverrideAtPath = Path extends undefined - ? DeepOverride - : Path extends `${infer P1}.${infer P2}` - ? P1 extends keyof T - ? Omit & Record>> - : never - : Path extends keyof T - ? Omit & Record> - : never; - -// Utility type from 'trpc-nuxt' -export type KeysOf = Array; - -// Utility type from 'trpc-nuxt' -export type PickFrom> = T extends Array - ? T - : T extends Record - ? keyof T extends K[number] - ? T - : K[number] extends never - ? T - : Pick - : T; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/helper.ts deleted file mode 100644 index 183476950..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/helper.ts +++ /dev/null @@ -1,74 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { TRPCError } from '@trpc/server'; -import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; - -export async function checkMutate(promise: Promise): Promise { - try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - if (err.meta?.reason === 'RESULT_NOT_READABLE') { - // unable to readback data - return undefined; - } else { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }); - } - } else { - throw err; - } - } - -} - -export async function checkRead(promise: Promise): Promise { - try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } else if (err.code === 'P2025') { - // not found - throw new TRPCError({ - code: 'NOT_FOUND', - message: err.message, - cause: err, - }); - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }) - } - } else { - throw err; - } - } - -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/Post.router.ts deleted file mode 100644 index f3cf556bf..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/Post.router.ts +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; -import * as _Schema from '@zenstackhq/runtime/zod/input'; -const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; -import { checkRead, checkMutate } from '../helper'; - -export default function createRouter(router: RouterFactory, procedure: ProcBuilder) { - return router({ - - aggregate: procedure.input($Schema.PostInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).post.aggregate(input as any))), - - createMany: procedure.input($Schema.PostInputSchema.createMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.createMany(input as any))), - - create: procedure.input($Schema.PostInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.create(input as any))), - - deleteMany: procedure.input($Schema.PostInputSchema.deleteMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.deleteMany(input as any))), - - delete: procedure.input($Schema.PostInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.delete(input as any))), - - findFirst: procedure.input($Schema.PostInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findFirst(input as any))), - - findFirstOrThrow: procedure.input($Schema.PostInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findFirstOrThrow(input as any))), - - findMany: procedure.input($Schema.PostInputSchema.findMany.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findMany(input as any))), - - findUnique: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUnique(input as any))), - - findUniqueOrThrow: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUniqueOrThrow(input as any))), - - groupBy: procedure.input($Schema.PostInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).post.groupBy(input as any))), - - updateMany: procedure.input($Schema.PostInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.updateMany(input as any))), - - update: procedure.input($Schema.PostInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.update(input as any))), - - upsert: procedure.input($Schema.PostInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.upsert(input as any))), - - count: procedure.input($Schema.PostInputSchema.count.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.count(input as any))), - - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/User.router.ts deleted file mode 100644 index a7d04017e..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/User.router.ts +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; -import * as _Schema from '@zenstackhq/runtime/zod/input'; -const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; -import { checkRead, checkMutate } from '../helper'; - -export default function createRouter(router: RouterFactory, procedure: ProcBuilder) { - return router({ - - aggregate: procedure.input($Schema.UserInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).user.aggregate(input as any))), - - createMany: procedure.input($Schema.UserInputSchema.createMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.createMany(input as any))), - - create: procedure.input($Schema.UserInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.create(input as any))), - - deleteMany: procedure.input($Schema.UserInputSchema.deleteMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.deleteMany(input as any))), - - delete: procedure.input($Schema.UserInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.delete(input as any))), - - findFirst: procedure.input($Schema.UserInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findFirst(input as any))), - - findFirstOrThrow: procedure.input($Schema.UserInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findFirstOrThrow(input as any))), - - findMany: procedure.input($Schema.UserInputSchema.findMany.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findMany(input as any))), - - findUnique: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUnique(input as any))), - - findUniqueOrThrow: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUniqueOrThrow(input as any))), - - groupBy: procedure.input($Schema.UserInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).user.groupBy(input as any))), - - updateMany: procedure.input($Schema.UserInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.updateMany(input as any))), - - update: procedure.input($Schema.UserInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.update(input as any))), - - upsert: procedure.input($Schema.UserInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.upsert(input as any))), - - count: procedure.input($Schema.UserInputSchema.count.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.count(input as any))), - - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/index.ts deleted file mode 100644 index 2925db282..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/generated/routers/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { unsetMarker, AnyRouter, AnyRootConfig, CreateRouterInner, Procedure, ProcedureBuilder, ProcedureParams, ProcedureRouterRecord, ProcedureType } from "@trpc/server"; -import type { PrismaClient } from "@prisma/client"; -import createUserRouter from "./User.router"; -import createPostRouter from "./Post.router"; - -export type BaseConfig = AnyRootConfig; - -export type RouterFactory = < - ProcRouterRecord extends ProcedureRouterRecord ->( - procedures: ProcRouterRecord -) => CreateRouterInner; - -export type UnsetMarker = typeof unsetMarker; - -export type ProcBuilder = ProcedureBuilder< - ProcedureParams ->; - -export function db(ctx: any) { - if (!ctx.prisma) { - throw new Error('Missing "prisma" field in trpc context'); - } - return ctx.prisma as PrismaClient; -} - -export function createRouter(router: RouterFactory, procedure: ProcBuilder) { - return router({ - user: createUserRouter(router, procedure), - post: createPostRouter(router, procedure), - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/index.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/index.ts deleted file mode 100644 index a73321b66..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/routers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { publicProcedure, router } from '../trpc'; -import { createRouter as createCRUDRouter } from './generated/routers'; - -export const appRouter = createCRUDRouter(router, publicProcedure); - -// export type definition of API -export type AppRouter = typeof appRouter; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/trpc.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/trpc.ts deleted file mode 100644 index 0b93ecda7..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/trpc/trpc.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This is your entry point to setup the root configuration for tRPC on the server. - * - `initTRPC` should only be used once per app. - * - We export only the functionality that we use so we can enforce which base procedures should be used - * - * Learn how to create protected base procedures and other things below: - * @see https://trpc.io/docs/v10/router - * @see https://trpc.io/docs/v10/procedures - */ -import { initTRPC } from '@trpc/server'; -import { Context } from './context'; - -const t = initTRPC.context().create(); - -/** - * Unprotected procedure - **/ -export const publicProcedure = t.procedure; -export const router = t.router; -export const middleware = t.middleware; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/tsconfig.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/tsconfig.json deleted file mode 100644 index b9ed69c19..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/server/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../.nuxt/tsconfig.server.json" -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/tsconfig.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/tsconfig.json deleted file mode 100644 index f80f43a36..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v10/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - // https://nuxt.com/docs/guide/concepts/typescript - "extends": "./.nuxt/tsconfig.json", - "compilerOptions": { - "verbatimModuleSyntax": false - } -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/.gitignore b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/.gitignore deleted file mode 100644 index cdb506446..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# Nuxt dev/build outputs -.output -.data -.nuxt -.nitro -.cache -dist - -# Node dependencies -node_modules - -# Logs -logs -*.log - -# Misc -.DS_Store -.fleet -.idea - -# Local env files -.env -.env.* -!.env.example - -*.db diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/README.md b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/README.md deleted file mode 100644 index f5db2a2db..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Nuxt 3 Minimal Starter - -Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. - -## Setup - -Make sure to install the dependencies: - -```bash -# npm -npm install - -# pnpm -pnpm install - -# yarn -yarn install - -# bun -bun install -``` - -## Development Server - -Start the development server on `http://localhost:3000`: - -```bash -# npm -npm run dev - -# pnpm -pnpm run dev - -# yarn -yarn dev - -# bun -bun run dev -``` - -## Production - -Build the application for production: - -```bash -# npm -npm run build - -# pnpm -pnpm run build - -# yarn -yarn build - -# bun -bun run build -``` - -Locally preview production build: - -```bash -# npm -npm run preview - -# pnpm -pnpm run preview - -# yarn -yarn preview - -# bun -bun run preview -``` - -Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/app.vue b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/app.vue deleted file mode 100644 index 8b526f9ed..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/app.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/nuxt.config.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/nuxt.config.ts deleted file mode 100644 index 897b1221c..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/nuxt.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -// https://nuxt.com/docs/api/configuration/nuxt-config -export default defineNuxtConfig({ - compatibilityDate: '2024-04-03', - devtools: { enabled: true }, - build: { - transpile: ['trpc-nuxt'], - }, -}); diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package-lock.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package-lock.json deleted file mode 100644 index 702316fd3..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package-lock.json +++ /dev/null @@ -1,9061 +0,0 @@ -{ - "name": "nuxt-app", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nuxt-app", - "hasInstallScript": true, - "dependencies": { - "@prisma/client": "6.8.x", - "@trpc/client": "^11.0.0-rc.563", - "@trpc/server": "^11.0.0-rc.563", - "nuxt": "^3.14.1592", - "trpc-nuxt": "^0.11.0-beta.1", - "vue": "latest", - "vue-router": "latest", - "zod": "^3.22.4" - }, - "devDependencies": { - "esbuild": "^0.24.0", - "prisma": "6.8.x", - "typescript": "^5.6.2", - "vue-tsc": "^2.1.10" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@antfu/utils": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", - "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", - "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", - "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", - "dependencies": { - "@babel/types": "^7.26.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.9.tgz", - "integrity": "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-decorators": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz", - "integrity": "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", - "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/standalone": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.26.2.tgz", - "integrity": "sha512-i2VbegsRfwa9yq3xmfDX3tG2yh9K0cCqwpSyVG2nPxifh0EOnucAZUeO/g4lW2Zfg03aPJNtPfxQbDHzXc7H+w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", - "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", - "dependencies": { - "mime": "^3.0.0" - }, - "engines": { - "node": ">=16.13" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", - "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", - "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "dependencies": { - "debug": "^4.1.1" - } - }, - "node_modules/@kwsites/promise-deferred": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@netlify/functions": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.8.2.tgz", - "integrity": "sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==", - "dependencies": { - "@netlify/serverless-functions-api": "1.26.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@netlify/node-cookies": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@netlify/node-cookies/-/node-cookies-0.1.0.tgz", - "integrity": "sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g==", - "engines": { - "node": "^14.16.0 || >=16.0.0" - } - }, - "node_modules/@netlify/serverless-functions-api": { - "version": "1.26.1", - "resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.26.1.tgz", - "integrity": "sha512-q3L9i3HoNfz0SGpTIS4zTcKBbRkxzCRpd169eyiTuk3IwcPC3/85mzLHranlKo2b+HYT0gu37YxGB45aD8A3Tw==", - "dependencies": { - "@netlify/node-cookies": "^0.1.0", - "urlpattern-polyfill": "8.0.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nuxt/devalue": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz", - "integrity": "sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==" - }, - "node_modules/@nuxt/devtools": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools/-/devtools-1.6.1.tgz", - "integrity": "sha512-s+4msaf8/REaXVbBDzjMgdUmEwR68hpoiQWx4QkH0JHSNQXWCWgNngqlZOM3DSRmPrelS57PJCag+L7gnT1wLw==", - "dependencies": { - "@antfu/utils": "^0.7.10", - "@nuxt/devtools-kit": "1.6.1", - "@nuxt/devtools-wizard": "1.6.1", - "@nuxt/kit": "^3.13.2", - "@vue/devtools-core": "7.6.4", - "@vue/devtools-kit": "7.6.4", - "birpc": "^0.2.17", - "consola": "^3.2.3", - "cronstrue": "^2.50.0", - "destr": "^2.0.3", - "error-stack-parser-es": "^0.1.5", - "execa": "^7.2.0", - "fast-npm-meta": "^0.2.2", - "flatted": "^3.3.1", - "get-port-please": "^3.1.2", - "hookable": "^5.5.3", - "image-meta": "^0.2.1", - "is-installed-globally": "^1.0.0", - "launch-editor": "^2.9.1", - "local-pkg": "^0.5.0", - "magicast": "^0.3.5", - "nypm": "^0.3.11", - "ohash": "^1.1.4", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.0", - "rc9": "^2.1.2", - "scule": "^1.3.0", - "semver": "^7.6.3", - "simple-git": "^3.27.0", - "sirv": "^2.0.4", - "tinyglobby": "^0.2.6", - "unimport": "^3.12.0", - "vite-plugin-inspect": "^0.8.7", - "vite-plugin-vue-inspector": "5.1.3", - "which": "^3.0.1", - "ws": "^8.18.0" - }, - "bin": { - "devtools": "cli.mjs" - }, - "peerDependencies": { - "vite": "*" - } - }, - "node_modules/@nuxt/devtools-kit": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-1.6.1.tgz", - "integrity": "sha512-6pvK5ih4XONVMSABlDbq6q7/TrZ++hyXGn5zdROVU780aYX3EjU8F0sq+1Lmc6ieiJg4tNe/EA+zV1onKRPsrQ==", - "dependencies": { - "@nuxt/kit": "^3.13.2", - "@nuxt/schema": "^3.13.2", - "execa": "^7.2.0" - }, - "peerDependencies": { - "vite": "*" - } - }, - "node_modules/@nuxt/devtools-wizard": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-1.6.1.tgz", - "integrity": "sha512-MpcKHgXJd4JyhJEvcIMTZqojyDFHLt9Wx2oWbV7YSEnubtHYxUM6p2M+Nb9/3mT+qoOiZQ+0db3xVcMW92oE8Q==", - "dependencies": { - "consola": "^3.2.3", - "diff": "^7.0.0", - "execa": "^7.2.0", - "global-directory": "^4.0.1", - "magicast": "^0.3.5", - "pathe": "^1.1.2", - "pkg-types": "^1.2.0", - "prompts": "^2.4.2", - "rc9": "^2.1.2", - "semver": "^7.6.3" - }, - "bin": { - "devtools-wizard": "cli.mjs" - } - }, - "node_modules/@nuxt/kit": { - "version": "3.14.1592", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.14.1592.tgz", - "integrity": "sha512-r9r8bISBBisvfcNgNL3dSIQHSBe0v5YkX5zwNblIC2T0CIEgxEVoM5rq9O5wqgb5OEydsHTtT2hL57vdv6VT2w==", - "dependencies": { - "@nuxt/schema": "3.14.1592", - "c12": "^2.0.1", - "consola": "^3.2.3", - "defu": "^6.1.4", - "destr": "^2.0.3", - "globby": "^14.0.2", - "hash-sum": "^2.0.0", - "ignore": "^6.0.2", - "jiti": "^2.4.0", - "klona": "^2.0.6", - "knitwork": "^1.1.0", - "mlly": "^1.7.3", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", - "scule": "^1.3.0", - "semver": "^7.6.3", - "ufo": "^1.5.4", - "unctx": "^2.3.1", - "unimport": "^3.13.2", - "untyped": "^1.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/@nuxt/schema": { - "version": "3.14.1592", - "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.14.1592.tgz", - "integrity": "sha512-A1d/08ueX8stTXNkvGqnr1eEXZgvKn+vj6s7jXhZNWApUSqMgItU4VK28vrrdpKbjIPwq2SwhnGOHUYvN9HwCQ==", - "dependencies": { - "c12": "^2.0.1", - "compatx": "^0.1.8", - "consola": "^3.2.3", - "defu": "^6.1.4", - "hookable": "^5.5.3", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", - "scule": "^1.3.0", - "std-env": "^3.8.0", - "ufo": "^1.5.4", - "uncrypto": "^0.1.3", - "unimport": "^3.13.2", - "untyped": "^1.5.1" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/@nuxt/telemetry": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.6.0.tgz", - "integrity": "sha512-h4YJ1d32cU7tDKjjhjtIIEck4WF/w3DTQBT348E9Pz85YLttnLqktLM0Ez9Xc2LzCeUgBDQv1el7Ob/zT3KUqg==", - "dependencies": { - "@nuxt/kit": "^3.13.1", - "ci-info": "^4.0.0", - "consola": "^3.2.3", - "create-require": "^1.1.1", - "defu": "^6.1.4", - "destr": "^2.0.3", - "dotenv": "^16.4.5", - "git-url-parse": "^15.0.0", - "is-docker": "^3.0.0", - "jiti": "^1.21.6", - "mri": "^1.2.0", - "nanoid": "^5.0.7", - "ofetch": "^1.3.4", - "package-manager-detector": "^0.2.0", - "parse-git-config": "^3.0.0", - "pathe": "^1.1.2", - "rc9": "^2.1.2", - "std-env": "^3.7.0" - }, - "bin": { - "nuxt-telemetry": "bin/nuxt-telemetry.mjs" - } - }, - "node_modules/@nuxt/telemetry/node_modules/jiti": { - "version": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/@nuxt/vite-builder": { - "version": "3.14.1592", - "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-3.14.1592.tgz", - "integrity": "sha512-GVS7vkBJAGv13ghmjgGrS2QVyzoqxQ5+cAUrMeMjKbY7GnRY7/uOkoLmznYx8E/U9HBUyHQa+wSN2ZfcSiEytQ==", - "dependencies": { - "@nuxt/kit": "3.14.1592", - "@rollup/plugin-replace": "^6.0.1", - "@vitejs/plugin-vue": "^5.2.0", - "@vitejs/plugin-vue-jsx": "^4.1.0", - "autoprefixer": "^10.4.20", - "clear": "^0.1.0", - "consola": "^3.2.3", - "cssnano": "^7.0.6", - "defu": "^6.1.4", - "esbuild": "^0.24.0", - "escape-string-regexp": "^5.0.0", - "estree-walker": "^3.0.3", - "externality": "^1.0.2", - "get-port-please": "^3.1.2", - "h3": "^1.13.0", - "jiti": "^2.4.0", - "knitwork": "^1.1.0", - "magic-string": "^0.30.13", - "mlly": "^1.7.3", - "ohash": "^1.1.4", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.1", - "postcss": "^8.4.49", - "rollup-plugin-visualizer": "^5.12.0", - "std-env": "^3.8.0", - "strip-literal": "^2.1.0", - "ufo": "^1.5.4", - "unenv": "^1.10.0", - "unplugin": "^1.16.0", - "vite": "^5.4.11", - "vite-node": "^2.1.5", - "vite-plugin-checker": "^0.8.0", - "vue-bundle-renderer": "^2.1.1" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - }, - "peerDependencies": { - "vue": "^3.3.4" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", - "hasInstallScript": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-wasm": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.5.0.tgz", - "integrity": "sha512-Z4ouuR8Pfggk1EYYbTaIoxc+Yv4o7cGQnH0Xy8+pQ+HbiW+ZnwhcD2LPf/prfq1nIWpAxjOkQ8uSMFWMtBLiVQ==", - "bundleDependencies": [ - "napi-wasm" - ], - "dependencies": { - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "napi-wasm": "^1.1.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" - }, - "node_modules/@prisma/client": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz", - "integrity": "sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==", - "hasInstallScript": true, - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@prisma/config": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.7.0.tgz", - "integrity": "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA==", - "devOptional": true, - "dependencies": { - "esbuild": ">=0.12 <1", - "esbuild-register": "3.6.0" - } - }, - "node_modules/@prisma/debug": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.7.0.tgz", - "integrity": "sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w==", - "devOptional": true - }, - "node_modules/@prisma/engines": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.7.0.tgz", - "integrity": "sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw==", - "devOptional": true, - "hasInstallScript": true, - "dependencies": { - "@prisma/debug": "6.7.0", - "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "@prisma/fetch-engine": "6.7.0", - "@prisma/get-platform": "6.7.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed.tgz", - "integrity": "sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA==", - "devOptional": true - }, - "node_modules/@prisma/fetch-engine": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.7.0.tgz", - "integrity": "sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ==", - "devOptional": true, - "dependencies": { - "@prisma/debug": "6.7.0", - "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "@prisma/get-platform": "6.7.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.7.0.tgz", - "integrity": "sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw==", - "devOptional": true, - "dependencies": { - "@prisma/debug": "6.7.0" - } - }, - "node_modules/@redocly/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js-replace": "^1.0.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@redocly/config": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.17.1.tgz", - "integrity": "sha512-CEmvaJuG7pm2ylQg53emPmtgm4nW2nxBgwXzbVEHpGas/lGnMyN8Zlkgiz6rPw0unASg6VW3wlz27SOL5XFHYQ==" - }, - "node_modules/@redocly/openapi-core": { - "version": "1.25.15", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.25.15.tgz", - "integrity": "sha512-/dpr5zpGj2t1Bf7EIXEboRZm1hsJZBQfv3Q1pkivtdAEg3if2khv+b9gY68aquC6cM/2aQY2kMLy8LlY2tn+Og==", - "dependencies": { - "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.17.0", - "colorette": "^1.2.0", - "https-proxy-agent": "^7.0.4", - "js-levenshtein": "^1.1.6", - "js-yaml": "^4.1.0", - "lodash.isequal": "^4.5.0", - "minimatch": "^5.0.1", - "node-fetch": "^2.6.1", - "pluralize": "^8.0.0", - "yaml-ast-parser": "0.0.43" - }, - "engines": { - "node": ">=14.19.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@redocly/openapi-core/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@redocly/openapi-core/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@rollup/plugin-alias": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz", - "integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", - "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@rollup/plugin-inject": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", - "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-inject/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", - "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", - "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", - "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz", - "integrity": "sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz", - "integrity": "sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz", - "integrity": "sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz", - "integrity": "sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz", - "integrity": "sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz", - "integrity": "sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz", - "integrity": "sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz", - "integrity": "sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz", - "integrity": "sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz", - "integrity": "sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz", - "integrity": "sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz", - "integrity": "sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz", - "integrity": "sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz", - "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz", - "integrity": "sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz", - "integrity": "sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz", - "integrity": "sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz", - "integrity": "sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@trpc/client": { - "version": "11.0.0-rc.563", - "resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.0.0-rc.563.tgz", - "integrity": "sha512-N1ldp7NGduuWoj/cuAQXvKkp/2MM3eVteRGLxuOgD5HzCKoDaJRSR/7EUm0qz8o51TW1LMbdoiYsM5Ip1lmXdA==", - "funding": [ - "https://trpc.io/sponsor" - ], - "peerDependencies": { - "@trpc/server": "11.0.0-rc.563+e5ae464b2" - } - }, - "node_modules/@trpc/server": { - "version": "11.0.0-rc.563", - "resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.0.0-rc.563.tgz", - "integrity": "sha512-yB7oNWIIrZTavlETrQeijo+JUh6CTeQTrqlMCJ7qHfqyadrfxa7O4JJ5pk9J5nur0sxiACwFi0RiQxSlrOVb2A==", - "funding": [ - "https://trpc.io/sponsor" - ] - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.15", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", - "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" - }, - "node_modules/@unhead/dom": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.13.tgz", - "integrity": "sha512-8Bpo3e50i49/z0TMiskQk3OqUVJpWOO0cnEEydJeFnjsPczDH76H3mWLvB11cv1B/rjLdBiPgui7yetFta5LCw==", - "dependencies": { - "@unhead/schema": "1.11.13", - "@unhead/shared": "1.11.13" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/@unhead/schema": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.13.tgz", - "integrity": "sha512-fIpQx6GCpl99l4qJXsPqkXxO7suMccuLADbhaMSkeXnVEi4ZIle+l+Ri0z+GHAEpJj17FMaQdO5n9FMSOMUxkw==", - "dependencies": { - "hookable": "^5.5.3", - "zhead": "^2.2.4" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/@unhead/shared": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.13.tgz", - "integrity": "sha512-EiJ3nsEtf6dvZ6OwVYrrrrCUl4ZE/9GTjpexEMti8EJXweSuL7SifNNXtIFk7UMoM0ULYxb7K/AKQV/odwoZyQ==", - "dependencies": { - "@unhead/schema": "1.11.13" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/@unhead/ssr": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/ssr/-/ssr-1.11.13.tgz", - "integrity": "sha512-LjomDIH8vXbnQQ8UVItmJ52BZBOyK12i1Q4W658X/f0VGtm0z3AulGQIvYla0rFcxAynDygfvWSC7xrlqDtRUw==", - "dependencies": { - "@unhead/schema": "1.11.13", - "@unhead/shared": "1.11.13" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/@unhead/vue": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.13.tgz", - "integrity": "sha512-s5++LqsNM01rkMQwtc4W19cP1fXC81o4YMyL+Kaqh9X0OPLeWnjONAh0U/Z2CIXBqhJHI+DoNXmDACXyuWPPxg==", - "dependencies": { - "@unhead/schema": "1.11.13", - "@unhead/shared": "1.11.13", - "defu": "^6.1.4", - "hookable": "^5.5.3", - "unhead": "1.11.13" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - }, - "peerDependencies": { - "vue": ">=2.7 || >=3" - } - }, - "node_modules/@vercel/nft": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.27.7.tgz", - "integrity": "sha512-FG6H5YkP4bdw9Ll1qhmbxuE8KwW2E/g8fJpM183fWQLeVDGqzeywMIeJ9h2txdWZ03psgWMn6QymTxaDLmdwUg==", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "@rollup/pluginutils": "^5.1.3", - "acorn": "^8.6.0", - "acorn-import-attributes": "^1.9.5", - "async-sema": "^3.1.1", - "bindings": "^1.4.0", - "estree-walker": "2.0.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.8", - "node-gyp-build": "^4.2.2", - "resolve-from": "^5.0.0" - }, - "bin": { - "nft": "out/cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@vercel/nft/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", - "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@vitejs/plugin-vue-jsx": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.1.1.tgz", - "integrity": "sha512-uMJqv/7u1zz/9NbWAD3XdjaY20tKTf17XVfQ9zq4wY1BjsB/PjpJPMe2xiG39QpP4ZdhYNhm4Hvo66uJrykNLA==", - "dependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-typescript": "^7.25.9", - "@vue/babel-plugin-jsx": "^1.2.5" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", - "vue": "^3.0.0" - } - }, - "node_modules/@volar/language-core": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz", - "integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==", - "devOptional": true, - "dependencies": { - "@volar/source-map": "2.4.10" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz", - "integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==", - "devOptional": true - }, - "node_modules/@volar/typescript": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz", - "integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==", - "devOptional": true, - "dependencies": { - "@volar/language-core": "2.4.10", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } - }, - "node_modules/@vue-macros/common": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-1.15.0.tgz", - "integrity": "sha512-yg5VqW7+HRfJGimdKvFYzx8zorHUYo0hzPwuraoC1DWa7HHazbTMoVsHDvk3JHa1SGfSL87fRnzmlvgjEHhszA==", - "dependencies": { - "@babel/types": "^7.25.8", - "@rollup/pluginutils": "^5.1.2", - "@vue/compiler-sfc": "^3.5.12", - "ast-kit": "^1.3.0", - "local-pkg": "^0.5.0", - "magic-string-ast": "^0.6.2" - }, - "engines": { - "node": ">=16.14.0" - }, - "peerDependencies": { - "vue": "^2.7.0 || ^3.2.25" - }, - "peerDependenciesMeta": { - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/babel-helper-vue-transform-on": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.5.tgz", - "integrity": "sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==" - }, - "node_modules/@vue/babel-plugin-jsx": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.5.tgz", - "integrity": "sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.6", - "@babel/types": "^7.25.6", - "@vue/babel-helper-vue-transform-on": "1.2.5", - "@vue/babel-plugin-resolve-type": "1.2.5", - "html-tags": "^3.3.1", - "svg-tags": "^1.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - } - } - }, - "node_modules/@vue/babel-plugin-resolve-type": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.5.tgz", - "integrity": "sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/parser": "^7.25.6", - "@vue/compiler-sfc": "^3.5.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", - "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" - } - }, - "node_modules/@vue/compiler-core/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", - "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", - "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", - "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" - } - }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", - "devOptional": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "license": "MIT" - }, - "node_modules/@vue/devtools-core": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.6.4.tgz", - "integrity": "sha512-blSwGVYpb7b5TALMjjoBiAl5imuBF7WEOAtaJaBMNikR8SQkm6mkUt4YlIKh9874/qoimwmpDOm+GHBZ4Y5m+g==", - "dependencies": { - "@vue/devtools-kit": "^7.6.4", - "@vue/devtools-shared": "^7.6.4", - "mitt": "^3.0.1", - "nanoid": "^3.3.4", - "pathe": "^1.1.2", - "vite-hot-client": "^0.2.3" - }, - "peerDependencies": { - "vue": "^3.0.0" - } - }, - "node_modules/@vue/devtools-core/node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/@vue/devtools-kit": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.4.tgz", - "integrity": "sha512-Zs86qIXXM9icU0PiGY09PQCle4TI750IPLmAJzW5Kf9n9t5HzSYf6Rz6fyzSwmfMPiR51SUKJh9sXVZu78h2QA==", - "dependencies": { - "@vue/devtools-shared": "^7.6.4", - "birpc": "^0.2.19", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.1" - } - }, - "node_modules/@vue/devtools-shared": { - "version": "7.6.7", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.7.tgz", - "integrity": "sha512-QggO6SviAsolrePAXZ/sA1dSicSPt4TueZibCvydfhNDieL1lAuyMTgQDGst7TEvMGb4vgYv2I+1sDkO4jWNnw==", - "dependencies": { - "rfdc": "^1.4.1" - } - }, - "node_modules/@vue/language-core": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz", - "integrity": "sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==", - "devOptional": true, - "dependencies": { - "@volar/language-core": "~2.4.8", - "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", - "@vue/shared": "^3.5.0", - "alien-signals": "^0.2.0", - "minimatch": "^9.0.3", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", - "dependencies": { - "@vue/shared": "3.5.13" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", - "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", - "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", - "csstype": "^3.1.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", - "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" - }, - "peerDependencies": { - "vue": "3.5.13" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/alien-signals": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz", - "integrity": "sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==", - "devOptional": true - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "node_modules/archiver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", - "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", - "dependencies": { - "archiver-utils": "^5.0.2", - "async": "^3.2.4", - "buffer-crc32": "^1.0.0", - "readable-stream": "^4.0.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^6.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", - "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", - "dependencies": { - "glob": "^10.0.0", - "graceful-fs": "^4.2.0", - "is-stream": "^2.0.1", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/archiver-utils/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/ast-kit": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.3.2.tgz", - "integrity": "sha512-gdvX700WVC6sHCJQ7bJGfDvtuKAh6Sa6weIZROxfzUZKP7BjvB8y0SMlM/o4omSQ3L60PQSJROBJsb0vEViVnA==", - "dependencies": { - "@babel/parser": "^7.26.2", - "pathe": "^1.1.2" - }, - "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/ast-walker-scope": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz", - "integrity": "sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==", - "dependencies": { - "@babel/parser": "^7.25.3", - "ast-kit": "^1.0.1" - }, - "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "node_modules/async-sema": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", - "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==" - }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/bare-events": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", - "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/birpc": { - "version": "0.2.19", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", - "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c12": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.1.tgz", - "integrity": "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==", - "dependencies": { - "chokidar": "^4.0.1", - "confbox": "^0.1.7", - "defu": "^6.1.4", - "dotenv": "^16.4.5", - "giget": "^1.2.3", - "jiti": "^2.3.0", - "mlly": "^1.7.1", - "ohash": "^1.1.4", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.0", - "rc9": "^2.1.2" - }, - "peerDependencies": { - "magicast": "^0.3.5" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001685", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001685.tgz", - "integrity": "sha512-e/kJN1EMyHQzgcMEEgoo+YTCO1NGCmIYHk5Qk8jT6AazWemS5QFKJ5ShCJlH3GZrNIdZofcNCEwZqbMjjKzmnA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", - "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/clear": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/clear/-/clear-0.1.0.tgz", - "integrity": "sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==", - "engines": { - "node": "*" - } - }, - "node_modules/clipboardy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz", - "integrity": "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==", - "dependencies": { - "execa": "^8.0.1", - "is-wsl": "^3.1.0", - "is64bit": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/clipboardy/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/clipboardy/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" - }, - "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" - }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "node_modules/compatx": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/compatx/-/compatx-0.1.8.tgz", - "integrity": "sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==" - }, - "node_modules/compress-commons": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", - "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/compress-commons/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==" - }, - "node_modules/consola": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", - "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/cookie-es": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", - "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==" - }, - "node_modules/copy-anything": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", - "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", - "dependencies": { - "is-what": "^4.1.8" - }, - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", - "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "node_modules/croner": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/croner/-/croner-9.0.0.tgz", - "integrity": "sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA==", - "engines": { - "node": ">=18.0" - } - }, - "node_modules/cronstrue": { - "version": "2.52.0", - "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.52.0.tgz", - "integrity": "sha512-NKgHbWkSZXJUcaBHSsyzC8eegD6bBd4O0oCI6XMIJ+y4Bq3v4w7sY3wfWoKPuVlq9pQHRB6od0lmKpIqi8TlKA==", - "bin": { - "cronstrue": "bin/cli.js" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crossws": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.1.tgz", - "integrity": "sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.6.tgz", - "integrity": "sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw==", - "dependencies": { - "cssnano-preset-default": "^7.0.6", - "lilconfig": "^3.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-preset-default": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.6.tgz", - "integrity": "sha512-ZzrgYupYxEvdGGuqL+JKOY70s7+saoNlHSCK/OGn1vB2pQK8KSET8jvenzItcY+kA7NoWvfbb/YhlzuzNKjOhQ==", - "dependencies": { - "browserslist": "^4.23.3", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.0", - "postcss-calc": "^10.0.2", - "postcss-colormin": "^7.0.2", - "postcss-convert-values": "^7.0.4", - "postcss-discard-comments": "^7.0.3", - "postcss-discard-duplicates": "^7.0.1", - "postcss-discard-empty": "^7.0.0", - "postcss-discard-overridden": "^7.0.0", - "postcss-merge-longhand": "^7.0.4", - "postcss-merge-rules": "^7.0.4", - "postcss-minify-font-values": "^7.0.0", - "postcss-minify-gradients": "^7.0.0", - "postcss-minify-params": "^7.0.2", - "postcss-minify-selectors": "^7.0.4", - "postcss-normalize-charset": "^7.0.0", - "postcss-normalize-display-values": "^7.0.0", - "postcss-normalize-positions": "^7.0.0", - "postcss-normalize-repeat-style": "^7.0.0", - "postcss-normalize-string": "^7.0.0", - "postcss-normalize-timing-functions": "^7.0.0", - "postcss-normalize-unicode": "^7.0.2", - "postcss-normalize-url": "^7.0.0", - "postcss-normalize-whitespace": "^7.0.0", - "postcss-ordered-values": "^7.0.1", - "postcss-reduce-initial": "^7.0.2", - "postcss-reduce-transforms": "^7.0.0", - "postcss-svgo": "^7.0.1", - "postcss-unique-selectors": "^7.0.3" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-utils": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", - "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/db0": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/db0/-/db0-0.2.1.tgz", - "integrity": "sha512-BWSFmLaCkfyqbSEZBQINMVNjCVfrogi7GQ2RSy1tmtfK9OXlsup6lUMwLsqSD7FbAjD04eWFdXowSHHUp6SE/Q==", - "peerDependencies": { - "@electric-sql/pglite": "*", - "@libsql/client": "*", - "better-sqlite3": "*", - "drizzle-orm": "*", - "mysql2": "*" - }, - "peerDependenciesMeta": { - "@electric-sql/pglite": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "drizzle-orm": { - "optional": true - }, - "mysql2": { - "optional": true - } - } - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "devOptional": true - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "engines": { - "node": ">=8" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destr": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", - "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==" - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==" - }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-prop": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", - "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", - "dependencies": { - "type-fest": "^4.18.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.68", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz", - "integrity": "sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-stack-parser-es": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", - "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/errx": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz", - "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==" - }, - "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" - }, - "node_modules/esbuild": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", - "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.0", - "@esbuild/android-arm": "0.24.0", - "@esbuild/android-arm64": "0.24.0", - "@esbuild/android-x64": "0.24.0", - "@esbuild/darwin-arm64": "0.24.0", - "@esbuild/darwin-x64": "0.24.0", - "@esbuild/freebsd-arm64": "0.24.0", - "@esbuild/freebsd-x64": "0.24.0", - "@esbuild/linux-arm": "0.24.0", - "@esbuild/linux-arm64": "0.24.0", - "@esbuild/linux-ia32": "0.24.0", - "@esbuild/linux-loong64": "0.24.0", - "@esbuild/linux-mips64el": "0.24.0", - "@esbuild/linux-ppc64": "0.24.0", - "@esbuild/linux-riscv64": "0.24.0", - "@esbuild/linux-s390x": "0.24.0", - "@esbuild/linux-x64": "0.24.0", - "@esbuild/netbsd-x64": "0.24.0", - "@esbuild/openbsd-arm64": "0.24.0", - "@esbuild/openbsd-x64": "0.24.0", - "@esbuild/sunos-x64": "0.24.0", - "@esbuild/win32-arm64": "0.24.0", - "@esbuild/win32-ia32": "0.24.0", - "@esbuild/win32-x64": "0.24.0" - } - }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", - "devOptional": true, - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" - } - }, - "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/externality": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz", - "integrity": "sha512-LyExtJWKxtgVzmgtEHyQtLFpw1KFhQphF9nTG8TpAIVkiI/xQ3FJh75tRFLYl4hkn7BNIIdLJInuDAavX35pMw==", - "dependencies": { - "enhanced-resolve": "^5.14.1", - "mlly": "^1.3.0", - "pathe": "^1.1.1", - "ufo": "^1.1.2" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-npm-meta": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fast-npm-meta/-/fast-npm-meta-0.2.2.tgz", - "integrity": "sha512-E+fdxeaOQGo/CMWc9f4uHFfgUPJRAu7N3uB8GBvB3SDPAIWJK4GKyYhkAGFq+GYrcbKNfQIz5VVQyJnDuPPCrg==", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==" - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-port-please": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", - "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==" - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/giget": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", - "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.2.3", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.3", - "nypm": "^0.3.8", - "ohash": "^1.1.3", - "pathe": "^1.1.2", - "tar": "^6.2.0" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, - "node_modules/git-config-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-2.0.0.tgz", - "integrity": "sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/git-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", - "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", - "dependencies": { - "is-ssh": "^1.4.0", - "parse-url": "^8.1.0" - } - }, - "node_modules/git-url-parse": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-15.0.0.tgz", - "integrity": "sha512-5reeBufLi+i4QD3ZFftcJs9jC26aULFLBU23FeKM/b1rI0K6ofIeAblmDVO7Ht22zTDE9+CkJ3ZVb0CgJmz3UQ==", - "dependencies": { - "git-up": "^7.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/globby": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", - "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/gzip-size": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz", - "integrity": "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/h3": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.13.0.tgz", - "integrity": "sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==", - "dependencies": { - "cookie-es": "^1.2.2", - "crossws": ">=0.2.0 <0.4.0", - "defu": "^6.1.4", - "destr": "^2.0.3", - "iron-webcrypto": "^1.2.1", - "ohash": "^1.1.4", - "radix3": "^1.1.2", - "ufo": "^1.5.4", - "uncrypto": "^0.1.3", - "unenv": "^1.10.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, - "node_modules/hash-sum": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", - "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==" - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "devOptional": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hookable": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" - }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-shutdown": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", - "integrity": "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/httpxy": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/httpxy/-/httpxy-0.1.5.tgz", - "integrity": "sha512-hqLDO+rfststuyEUTWObQK6zHEEmZ/kaIP2/zclGGZn6X8h/ESTWg+WKecQ/e5k4nPswjzZD+q2VqZIbr15CoQ==" - }, - "node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/image-meta": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.1.tgz", - "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==" - }, - "node_modules/impound": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/impound/-/impound-0.2.0.tgz", - "integrity": "sha512-gXgeSyp9Hf7qG2/PLKmywHXyQf2xFrw+mJGpoj9DsAB9L7/MIKn+DeEx98UryWXdmbv8wUUPdcQof6qXnZoCGg==", - "dependencies": { - "@rollup/pluginutils": "^5.1.2", - "mlly": "^1.7.2", - "pathe": "^1.1.2", - "unenv": "^1.10.0", - "unplugin": "^1.14.1" - } - }, - "node_modules/index-to-position": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz", - "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "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.", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ioredis": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", - "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/iron-webcrypto": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", - "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-installed-globally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", - "dependencies": { - "global-directory": "^4.0.1", - "is-path-inside": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-ssh": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", - "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", - "dependencies": { - "protocols": "^2.0.1" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-what": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is64bit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", - "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", - "dependencies": { - "system-architecture": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", - "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/knitwork": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.1.0.tgz", - "integrity": "sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==" - }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" - }, - "node_modules/launch-editor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", - "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" - } - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/listhen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.9.0.tgz", - "integrity": "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==", - "dependencies": { - "@parcel/watcher": "^2.4.1", - "@parcel/watcher-wasm": "^2.4.1", - "citty": "^0.1.6", - "clipboardy": "^4.0.0", - "consola": "^3.2.3", - "crossws": ">=0.2.0 <0.4.0", - "defu": "^6.1.4", - "get-port-please": "^3.1.2", - "h3": "^1.12.0", - "http-shutdown": "^1.2.2", - "jiti": "^2.1.2", - "mlly": "^1.7.1", - "node-forge": "^1.3.1", - "pathe": "^1.1.2", - "std-env": "^3.7.0", - "ufo": "^1.5.4", - "untun": "^0.1.3", - "uqr": "^0.1.2" - }, - "bin": { - "listen": "bin/listhen.mjs", - "listhen": "bin/listhen.mjs" - } - }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.14", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", - "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/magic-string-ast": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-0.6.3.tgz", - "integrity": "sha512-C9sgUzVZtUtzCBoMdYtwrIRQ4IucGRFGgdhkjL7PXsVfPYmTuWtewqzk7dlipaCMWH/gOYehW9rgMoa4Oebtpw==", - "dependencies": { - "magic-string": "^0.30.13" - }, - "engines": { - "node": ">=16.14.0" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mlly": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", - "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", - "ufo": "^1.5.4" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "devOptional": true - }, - "node_modules/nanoid": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", - "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/nanotar": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/nanotar/-/nanotar-0.1.1.tgz", - "integrity": "sha512-AiJsGsSF3O0havL1BydvI4+wR76sKT+okKRwWIaK96cZUnXqH0uNBOsHlbwZq3+m2BR1VKqHDVudl3gO4mYjpQ==" - }, - "node_modules/nitropack": { - "version": "2.10.4", - "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.10.4.tgz", - "integrity": "sha512-sJiG/MIQlZCVSw2cQrFG1H6mLeSqHlYfFerRjLKz69vUfdu0EL2l0WdOxlQbzJr3mMv/l4cOlCCLzVRzjzzF/g==", - "dependencies": { - "@cloudflare/kv-asset-handler": "^0.3.4", - "@netlify/functions": "^2.8.2", - "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-inject": "^5.0.5", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-replace": "^6.0.1", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/pluginutils": "^5.1.3", - "@types/http-proxy": "^1.17.15", - "@vercel/nft": "^0.27.5", - "archiver": "^7.0.1", - "c12": "2.0.1", - "chokidar": "^3.6.0", - "citty": "^0.1.6", - "compatx": "^0.1.8", - "confbox": "^0.1.8", - "consola": "^3.2.3", - "cookie-es": "^1.2.2", - "croner": "^9.0.0", - "crossws": "^0.3.1", - "db0": "^0.2.1", - "defu": "^6.1.4", - "destr": "^2.0.3", - "dot-prop": "^9.0.0", - "esbuild": "^0.24.0", - "escape-string-regexp": "^5.0.0", - "etag": "^1.8.1", - "fs-extra": "^11.2.0", - "globby": "^14.0.2", - "gzip-size": "^7.0.0", - "h3": "^1.13.0", - "hookable": "^5.5.3", - "httpxy": "^0.1.5", - "ioredis": "^5.4.1", - "jiti": "^2.4.0", - "klona": "^2.0.6", - "knitwork": "^1.1.0", - "listhen": "^1.9.0", - "magic-string": "^0.30.12", - "magicast": "^0.3.5", - "mime": "^4.0.4", - "mlly": "^1.7.2", - "node-fetch-native": "^1.6.4", - "ofetch": "^1.4.1", - "ohash": "^1.1.4", - "openapi-typescript": "^7.4.2", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.1", - "pretty-bytes": "^6.1.1", - "radix3": "^1.1.2", - "rollup": "^4.24.3", - "rollup-plugin-visualizer": "^5.12.0", - "scule": "^1.3.0", - "semver": "^7.6.3", - "serve-placeholder": "^2.0.2", - "serve-static": "^1.16.2", - "std-env": "^3.7.0", - "ufo": "^1.5.4", - "uncrypto": "^0.1.3", - "unctx": "^2.3.1", - "unenv": "^1.10.0", - "unimport": "^3.13.1", - "unstorage": "^1.13.1", - "untyped": "^1.5.1", - "unwasm": "^0.3.9" - }, - "bin": { - "nitro": "dist/cli/index.mjs", - "nitropack": "dist/cli/index.mjs" - }, - "engines": { - "node": "^16.11.0 || >=17.0.0" - }, - "peerDependencies": { - "xml2js": "^0.6.2" - }, - "peerDependenciesMeta": { - "xml2js": { - "optional": true - } - } - }, - "node_modules/nitropack/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/nitropack/node_modules/mime": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", - "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", - "funding": [ - "https://github.com/sponsors/broofa" - ], - "bin": { - "mime": "bin/cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/nitropack/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/nitropack/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", - "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==" - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nuxi": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/nuxi/-/nuxi-3.16.0.tgz", - "integrity": "sha512-t9m4zTq44R0/icuzQXJHEyPRM3YbgTPMpytyb6YW2LOL/3mwZ3Bmte1FIlCoigzDvxBJRbcchZGc689+Syyu8w==", - "bin": { - "nuxi": "bin/nuxi.mjs", - "nuxi-ng": "bin/nuxi.mjs", - "nuxt": "bin/nuxi.mjs", - "nuxt-cli": "bin/nuxi.mjs" - }, - "engines": { - "node": "^16.10.0 || >=18.0.0" - } - }, - "node_modules/nuxt": { - "version": "3.14.1592", - "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-3.14.1592.tgz", - "integrity": "sha512-roWAQH4Mb6WY72cNos+YVw0DgTCNAhNygiAMCedM7hbX6ESTR2n3VH7tU0yIWDPe/hfFdii4M4wWTTNHOtS44g==", - "dependencies": { - "@nuxt/devalue": "^2.0.2", - "@nuxt/devtools": "^1.6.0", - "@nuxt/kit": "3.14.1592", - "@nuxt/schema": "3.14.1592", - "@nuxt/telemetry": "^2.6.0", - "@nuxt/vite-builder": "3.14.1592", - "@unhead/dom": "^1.11.11", - "@unhead/shared": "^1.11.11", - "@unhead/ssr": "^1.11.11", - "@unhead/vue": "^1.11.11", - "@vue/shared": "^3.5.13", - "acorn": "8.14.0", - "c12": "^2.0.1", - "chokidar": "^4.0.1", - "compatx": "^0.1.8", - "consola": "^3.2.3", - "cookie-es": "^1.2.2", - "defu": "^6.1.4", - "destr": "^2.0.3", - "devalue": "^5.1.1", - "errx": "^0.1.0", - "esbuild": "^0.24.0", - "escape-string-regexp": "^5.0.0", - "estree-walker": "^3.0.3", - "globby": "^14.0.2", - "h3": "^1.13.0", - "hookable": "^5.5.3", - "ignore": "^6.0.2", - "impound": "^0.2.0", - "jiti": "^2.4.0", - "klona": "^2.0.6", - "knitwork": "^1.1.0", - "magic-string": "^0.30.13", - "mlly": "^1.7.3", - "nanotar": "^0.1.1", - "nitropack": "^2.10.4", - "nuxi": "^3.15.0", - "nypm": "^0.3.12", - "ofetch": "^1.4.1", - "ohash": "^1.1.4", - "pathe": "^1.1.2", - "perfect-debounce": "^1.0.0", - "pkg-types": "^1.2.1", - "radix3": "^1.1.2", - "scule": "^1.3.0", - "semver": "^7.6.3", - "std-env": "^3.8.0", - "strip-literal": "^2.1.0", - "tinyglobby": "0.2.10", - "ufo": "^1.5.4", - "ultrahtml": "^1.5.3", - "uncrypto": "^0.1.3", - "unctx": "^2.3.1", - "unenv": "^1.10.0", - "unhead": "^1.11.11", - "unimport": "^3.13.2", - "unplugin": "^1.16.0", - "unplugin-vue-router": "^0.10.8", - "unstorage": "^1.13.1", - "untyped": "^1.5.1", - "vue": "^3.5.13", - "vue-bundle-renderer": "^2.1.1", - "vue-devtools-stub": "^0.1.0", - "vue-router": "^4.4.5" - }, - "bin": { - "nuxi": "bin/nuxt.mjs", - "nuxt": "bin/nuxt.mjs" - }, - "engines": { - "node": "^14.18.0 || >=16.10.0" - }, - "peerDependencies": { - "@parcel/watcher": "^2.1.0", - "@types/node": "^14.18.0 || >=16.10.0" - }, - "peerDependenciesMeta": { - "@parcel/watcher": { - "optional": true - }, - "@types/node": { - "optional": true - } - } - }, - "node_modules/nypm": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", - "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.2.3", - "execa": "^8.0.1", - "pathe": "^1.1.2", - "pkg-types": "^1.2.0", - "ufo": "^1.5.4" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, - "node_modules/nypm/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/nypm/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/nypm/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ofetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", - "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", - "dependencies": { - "destr": "^2.0.3", - "node-fetch-native": "^1.6.4", - "ufo": "^1.5.4" - } - }, - "node_modules/ohash": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", - "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/openapi-typescript": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.4.4.tgz", - "integrity": "sha512-7j3nktnRzlQdlHnHsrcr6Gqz8f80/RhfA2I8s1clPI+jkY0hLNmnYVKBfuUEli5EEgK1B6M+ibdS5REasPlsUw==", - "dependencies": { - "@redocly/openapi-core": "^1.25.9", - "ansi-colors": "^4.1.3", - "parse-json": "^8.1.0", - "supports-color": "^9.4.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "openapi-typescript": "bin/cli.js" - }, - "peerDependencies": { - "typescript": "^5.x" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" - }, - "node_modules/package-manager-detector": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.6.tgz", - "integrity": "sha512-9vPH3qooBlYRJdmdYP00nvjZOulm40r5dhtal8st18ctf+6S1k7pi5yIHLvI4w5D70x0Y+xdVD9qITH0QO/A8A==" - }, - "node_modules/parse-git-config": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-3.0.0.tgz", - "integrity": "sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==", - "dependencies": { - "git-config-path": "^2.0.0", - "ini": "^1.3.5" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parse-git-config/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/parse-json": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", - "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "index-to-position": "^0.1.2", - "type-fest": "^4.7.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-path": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", - "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", - "dependencies": { - "protocols": "^2.0.0" - } - }, - "node_modules/parse-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", - "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", - "dependencies": { - "parse-path": "^7.0.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "devOptional": true - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", - "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.2", - "pathe": "^1.1.2" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-calc": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.2.tgz", - "integrity": "sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==", - "dependencies": { - "postcss-selector-parser": "^6.1.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12 || ^20.9 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.38" - } - }, - "node_modules/postcss-colormin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", - "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-convert-values": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.4.tgz", - "integrity": "sha512-e2LSXPqEHVW6aoGbjV9RsSSNDO3A0rZLCBxN24zvxF25WknMPpX8Dm9UxxThyEbaytzggRuZxaGXqaOhxQ514Q==", - "dependencies": { - "browserslist": "^4.23.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-comments": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.3.tgz", - "integrity": "sha512-q6fjd4WU4afNhWOA2WltHgCbkRhZPgQe7cXF74fuVB/ge4QbM9HEaOIzGSiMvM+g/cOsNAUGdf2JDzqA2F8iLA==", - "dependencies": { - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", - "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-empty": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", - "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", - "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.4.tgz", - "integrity": "sha512-zer1KoZA54Q8RVHKOY5vMke0cCdNxMP3KBfDerjH/BYHh4nCIh+1Yy0t1pAEQF18ac/4z3OFclO+ZVH8azjR4A==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^7.0.4" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-rules": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.4.tgz", - "integrity": "sha512-ZsaamiMVu7uBYsIdGtKJ64PkcQt6Pcpep/uO90EpLS3dxJi6OXamIobTYcImyXGoW0Wpugh7DSD3XzxZS9JCPg==", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^5.0.0", - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", - "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", - "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-params": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", - "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", - "dependencies": { - "browserslist": "^4.23.3", - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.4.tgz", - "integrity": "sha512-JG55VADcNb4xFCf75hXkzc1rNeURhlo7ugf6JjiiKRfMsKlDzN9CXHZDyiG6x/zGchpjQS+UAgb1d4nqXqOpmA==", - "dependencies": { - "cssesc": "^3.0.0", - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", - "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", - "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", - "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", - "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-string": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", - "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", - "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", - "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", - "dependencies": { - "browserslist": "^4.23.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", - "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", - "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-ordered-values": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", - "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", - "dependencies": { - "cssnano-utils": "^5.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", - "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", - "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", - "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.3.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.3.tgz", - "integrity": "sha512-J+58u5Ic5T1QjP/LDV9g3Cx4CNOgB5vz+kM6+OxHHhFACdcDeKhBXjQmB7fnIZM12YSTvsL0Opwco83DmacW2g==", - "dependencies": { - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prisma": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.7.0.tgz", - "integrity": "sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw==", - "devOptional": true, - "hasInstallScript": true, - "dependencies": { - "@prisma/config": "6.7.0", - "@prisma/engines": "6.7.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=18.18" - }, - "optionalDependencies": { - "fsevents": "2.3.3" - }, - "peerDependencies": { - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/protocols": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", - "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" - }, - "node_modules/radix3": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", - "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" - } - }, - "node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.0.tgz", - "integrity": "sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.0", - "@rollup/rollup-android-arm64": "4.28.0", - "@rollup/rollup-darwin-arm64": "4.28.0", - "@rollup/rollup-darwin-x64": "4.28.0", - "@rollup/rollup-freebsd-arm64": "4.28.0", - "@rollup/rollup-freebsd-x64": "4.28.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.0", - "@rollup/rollup-linux-arm-musleabihf": "4.28.0", - "@rollup/rollup-linux-arm64-gnu": "4.28.0", - "@rollup/rollup-linux-arm64-musl": "4.28.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.0", - "@rollup/rollup-linux-riscv64-gnu": "4.28.0", - "@rollup/rollup-linux-s390x-gnu": "4.28.0", - "@rollup/rollup-linux-x64-gnu": "4.28.0", - "@rollup/rollup-linux-x64-musl": "4.28.0", - "@rollup/rollup-win32-arm64-msvc": "4.28.0", - "@rollup/rollup-win32-ia32-msvc": "4.28.0", - "@rollup/rollup-win32-x64-msvc": "4.28.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-visualizer": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", - "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", - "dependencies": { - "open": "^8.4.0", - "picomatch": "^2.3.1", - "source-map": "^0.7.4", - "yargs": "^17.5.1" - }, - "bin": { - "rollup-plugin-visualizer": "dist/bin/cli.js" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "rollup": "2.x || 3.x || 4.x" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/scule": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", - "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==" - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-placeholder": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-2.0.2.tgz", - "integrity": "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==", - "dependencies": { - "defu": "^6.1.4" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/simple-git": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", - "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", - "dependencies": { - "@kwsites/file-exists": "^1.1.1", - "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.5" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/git-js?sponsor=1" - } - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" - }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/smob": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==" - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/speakingurl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", - "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==" - }, - "node_modules/streamx": { - "version": "2.20.2", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.2.tgz", - "integrity": "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==", - "dependencies": { - "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==" - }, - "node_modules/stylehacks": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz", - "integrity": "sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==", - "dependencies": { - "browserslist": "^4.23.3", - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/superjson": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", - "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", - "dependencies": { - "copy-anything": "^3.0.2" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==" - }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/system-architecture": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", - "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/text-decoder": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", - "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==" - }, - "node_modules/tinyglobby": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", - "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", - "dependencies": { - "fdir": "^6.4.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/trpc-nuxt": { - "version": "0.11.0-beta.1", - "resolved": "https://registry.npmjs.org/trpc-nuxt/-/trpc-nuxt-0.11.0-beta.1.tgz", - "integrity": "sha512-8RvTkHsRKXtyo9LLjZe3yilHif2vKtoDsQH6oZI7FI7a08Yguk9ln6hwPEWLaZcnzvrKotzXxSnhFMqjmZi7rg==", - "dependencies": { - "h3": "^1.11.1", - "ofetch": "^1.3.4", - "ohash": "^1.1.3" - }, - "peerDependencies": { - "@trpc/client": "^11.0.0-rc.1", - "@trpc/server": "^11.0.0-rc.1" - } - }, - "node_modules/type-fest": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz", - "integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.6.2", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", - "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==" - }, - "node_modules/ultrahtml": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", - "integrity": "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==" - }, - "node_modules/uncrypto": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", - "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" - }, - "node_modules/unctx": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.3.1.tgz", - "integrity": "sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==", - "dependencies": { - "acorn": "^8.8.2", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.0", - "unplugin": "^1.3.1" - } - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" - }, - "node_modules/unenv": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz", - "integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==", - "dependencies": { - "consola": "^3.2.3", - "defu": "^6.1.4", - "mime": "^3.0.0", - "node-fetch-native": "^1.6.4", - "pathe": "^1.1.2" - } - }, - "node_modules/unhead": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.13.tgz", - "integrity": "sha512-I7yyvqRfpPPzXuCG7HKZkgAWJDbzXDDEVyib4C/78HREqhNGHVSyo4TqX1h1xB5cx7WYc21HHDRT2/8YkqOy2w==", - "dependencies": { - "@unhead/dom": "1.11.13", - "@unhead/schema": "1.11.13", - "@unhead/shared": "1.11.13", - "hookable": "^5.5.3" - }, - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unimport": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.2.tgz", - "integrity": "sha512-FSxhbAylGGanyuTb3K0Ka3T9mnsD0+cRKbwOS11Li4Lh2whWS091e32JH4bIHrTckxlW9GnExAglADlxXjjzFw==", - "dependencies": { - "@rollup/pluginutils": "^5.1.3", - "acorn": "^8.14.0", - "escape-string-regexp": "^5.0.0", - "estree-walker": "^3.0.3", - "local-pkg": "^0.5.1", - "magic-string": "^0.30.14", - "mlly": "^1.7.3", - "pathe": "^1.1.2", - "picomatch": "^4.0.2", - "pkg-types": "^1.2.1", - "scule": "^1.3.0", - "strip-literal": "^2.1.1", - "tinyglobby": "^0.2.10", - "unplugin": "^1.16.0" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unplugin": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.0.tgz", - "integrity": "sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==", - "dependencies": { - "acorn": "^8.14.0", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/unplugin-vue-router": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.10.8.tgz", - "integrity": "sha512-xi+eLweYAqolIoTRSmumbi6Yx0z5M0PLvl+NFNVWHJgmE2ByJG1SZbrn+TqyuDtIyln20KKgq8tqmL7aLoiFjw==", - "dependencies": { - "@babel/types": "^7.25.4", - "@rollup/pluginutils": "^5.1.0", - "@vue-macros/common": "^1.12.2", - "ast-walker-scope": "^0.6.2", - "chokidar": "^3.6.0", - "fast-glob": "^3.3.2", - "json5": "^2.2.3", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.11", - "mlly": "^1.7.1", - "pathe": "^1.1.2", - "scule": "^1.3.0", - "unplugin": "^1.12.2", - "yaml": "^2.5.0" - }, - "peerDependencies": { - "vue-router": "^4.4.0" - }, - "peerDependenciesMeta": { - "vue-router": { - "optional": true - } - } - }, - "node_modules/unplugin-vue-router/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/unplugin-vue-router/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/unplugin-vue-router/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/unstorage": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.13.1.tgz", - "integrity": "sha512-ELexQHUrG05QVIM/iUeQNdl9FXDZhqLJ4yP59fnmn2jGUh0TEulwOgov1ubOb3Gt2ZGK/VMchJwPDNVEGWQpRg==", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^3.6.0", - "citty": "^0.1.6", - "destr": "^2.0.3", - "h3": "^1.13.0", - "listhen": "^1.9.0", - "lru-cache": "^10.4.3", - "node-fetch-native": "^1.6.4", - "ofetch": "^1.4.1", - "ufo": "^1.5.4" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.7.0", - "@azure/cosmos": "^4.1.1", - "@azure/data-tables": "^13.2.2", - "@azure/identity": "^4.5.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.25.0", - "@capacitor/preferences": "^6.0.2", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/kv": "^1.0.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.1" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - } - } - }, - "node_modules/unstorage/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/unstorage/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, - "node_modules/unstorage/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/unstorage/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/untun": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz", - "integrity": "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==", - "dependencies": { - "citty": "^0.1.5", - "consola": "^3.2.3", - "pathe": "^1.1.1" - }, - "bin": { - "untun": "bin/untun.mjs" - } - }, - "node_modules/untyped": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.1.tgz", - "integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==", - "dependencies": { - "@babel/core": "^7.25.7", - "@babel/standalone": "^7.25.7", - "@babel/types": "^7.25.7", - "defu": "^6.1.4", - "jiti": "^2.3.1", - "mri": "^1.2.0", - "scule": "^1.3.0" - }, - "bin": { - "untyped": "dist/cli.mjs" - } - }, - "node_modules/unwasm": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/unwasm/-/unwasm-0.3.9.tgz", - "integrity": "sha512-LDxTx/2DkFURUd+BU1vUsF/moj0JsoTvl+2tcg2AUOiEzVturhGGx17/IMgGvKUYdZwr33EJHtChCJuhu9Ouvg==", - "dependencies": { - "knitwork": "^1.0.0", - "magic-string": "^0.30.8", - "mlly": "^1.6.1", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "unplugin": "^1.10.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uqr": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", - "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==" - }, - "node_modules/uri-js-replace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", - "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==" - }, - "node_modules/urlpattern-polyfill": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", - "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-hot-client": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-0.2.4.tgz", - "integrity": "sha512-a1nzURqO7DDmnXqabFOliz908FRmIppkBKsJthS8rbe8hBEXwEwe4C3Pp33Z1JoFCYfVL4kTOMLKk0ZZxREIeA==", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" - } - }, - "node_modules/vite-node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", - "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-plugin-checker": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz", - "integrity": "sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "ansi-escapes": "^4.3.0", - "chalk": "^4.1.1", - "chokidar": "^3.5.1", - "commander": "^8.0.0", - "fast-glob": "^3.2.7", - "fs-extra": "^11.1.0", - "npm-run-path": "^4.0.1", - "strip-ansi": "^6.0.0", - "vscode-languageclient": "^7.0.0", - "vscode-languageserver": "^7.0.0", - "vscode-languageserver-textdocument": "^1.0.1", - "vscode-uri": "^3.0.2" - }, - "engines": { - "node": ">=14.16" - }, - "peerDependencies": { - "@biomejs/biome": ">=1.7", - "eslint": ">=7", - "meow": "^9.0.0", - "optionator": "^0.9.1", - "stylelint": ">=13", - "typescript": "*", - "vite": ">=2.0.0", - "vls": "*", - "vti": "*", - "vue-tsc": "~2.1.6" - }, - "peerDependenciesMeta": { - "@biomejs/biome": { - "optional": true - }, - "eslint": { - "optional": true - }, - "meow": { - "optional": true - }, - "optionator": { - "optional": true - }, - "stylelint": { - "optional": true - }, - "typescript": { - "optional": true - }, - "vls": { - "optional": true - }, - "vti": { - "optional": true - }, - "vue-tsc": { - "optional": true - } - } - }, - "node_modules/vite-plugin-checker/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/vite-plugin-checker/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/vite-plugin-checker/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/vite-plugin-checker/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vite-plugin-checker/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/vite-plugin-inspect": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.8.tgz", - "integrity": "sha512-aZlBuXsWUPJFmMK92GIv6lH7LrwG2POu4KJ+aEdcqnu92OAf+rhBnfMDQvxIJPEB7hE2t5EyY/PMgf5aDLT8EA==", - "dependencies": { - "@antfu/utils": "^0.7.10", - "@rollup/pluginutils": "^5.1.3", - "debug": "^4.3.7", - "error-stack-parser-es": "^0.1.5", - "fs-extra": "^11.2.0", - "open": "^10.1.0", - "perfect-debounce": "^1.0.0", - "picocolors": "^1.1.1", - "sirv": "^3.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } - } - }, - "node_modules/vite-plugin-inspect/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vite-plugin-inspect/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vite-plugin-inspect/node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite-plugin-vue-inspector": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.1.3.tgz", - "integrity": "sha512-pMrseXIDP1Gb38mOevY+BvtNGNqiqmqa2pKB99lnLsADQww9w9xMbAfT4GB6RUoaOkSPrtlXqpq2Fq+Dj2AgFg==", - "dependencies": { - "@babel/core": "^7.23.0", - "@babel/plugin-proposal-decorators": "^7.23.0", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-transform-typescript": "^7.22.15", - "@vue/babel-plugin-jsx": "^1.1.5", - "@vue/compiler-dom": "^3.3.4", - "kolorist": "^1.8.0", - "magic-string": "^0.30.4" - }, - "peerDependencies": { - "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", - "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", - "engines": { - "node": ">=8.0.0 || >=10.0.0" - } - }, - "node_modules/vscode-languageclient": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", - "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", - "dependencies": { - "minimatch": "^3.0.4", - "semver": "^7.3.4", - "vscode-languageserver-protocol": "3.16.0" - }, - "engines": { - "vscode": "^1.52.0" - } - }, - "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/vscode-languageclient/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/vscode-languageserver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", - "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", - "dependencies": { - "vscode-languageserver-protocol": "3.16.0" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", - "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", - "dependencies": { - "vscode-jsonrpc": "6.0.0", - "vscode-languageserver-types": "3.16.0" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", - "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" - }, - "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" - }, - "node_modules/vue": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", - "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-bundle-renderer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.1.tgz", - "integrity": "sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==", - "dependencies": { - "ufo": "^1.5.4" - } - }, - "node_modules/vue-devtools-stub": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", - "integrity": "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==" - }, - "node_modules/vue-router": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz", - "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/vue-tsc": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz", - "integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==", - "devOptional": true, - "dependencies": { - "@volar/typescript": "~2.4.8", - "@vue/language-core": "2.1.10", - "semver": "^7.5.4" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yaml-ast-parser": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/zhead": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", - "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", - "funding": { - "url": "https://github.com/sponsors/harlan-zw" - } - }, - "node_modules/zip-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", - "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", - "dependencies": { - "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.2", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/zod": { - "version": "3.23.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json deleted file mode 100644 index 9a14e9c21..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "nuxt-app", - "private": true, - "type": "module", - "scripts": { - "build": "nuxt typecheck && nuxt build", - "dev": "nuxt dev", - "generate": "nuxt generate", - "preview": "nuxt preview", - "postinstall": "nuxt prepare" - }, - "dependencies": { - "@prisma/client": "6.19.x", - "@trpc/client": "^11.0.0-rc.563", - "@trpc/server": "^11.0.0-rc.563", - "nuxt": "^3.14.1592", - "trpc-nuxt": "^0.11.0-beta.1", - "vue": "latest", - "vue-router": "latest", - "zod": "^3.25.0" - }, - "devDependencies": { - "esbuild": "^0.24.0", - "prisma": "6.19.x", - "typescript": "^5.6.2", - "vue-tsc": "^2.1.10" - } -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/plugins/client.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/plugins/client.ts deleted file mode 100644 index 9493be346..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/plugins/client.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { httpBatchLink } from 'trpc-nuxt/client'; -import type { AppRouter } from '~/server/trpc/routers'; -import { createTRPCNuxtClient } from '~/server/trpc/routers/generated/client/nuxt'; - -export default defineNuxtPlugin(() => { - /** - * createTRPCNuxtClient adds a `useQuery` composable - * built on top of `useAsyncData`. - */ - const client = createTRPCNuxtClient({ - links: [ - httpBatchLink({ - url: '/api/trpc', - }), - ], - }); - - return { - provide: { - client, - }, - }; -}); diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/prisma/schema.prisma b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/prisma/schema.prisma deleted file mode 100644 index 71cd1ce9b..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/prisma/schema.prisma +++ /dev/null @@ -1,31 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -////////////////////////////////////////////////////////////////////////////////////////////// - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -generator client { - provider = "prisma-client-js" -} - -model User { - id String @id() @default(cuid()) - email String @unique() - password String - posts Post[] -} - -model Post { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - title String - content String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/public/favicon.ico b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/public/favicon.ico deleted file mode 100644 index 18993ad91cfd43e03b074dd0b5cc3f37ab38e49c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmeHLOKuuL5PjK%MHWVi6lD zOGiREbCw`xmFozJ^aNatJY>w+g ze6a2@u~m#^BZm@8wco9#Crlli0uLb^3E$t2-WIc^#(?t)*@`UpuofJ(Uyh@F>b3Ph z$D^m8Xq~pTkGJ4Q`Q2)te3mgkWYZ^Ijq|hkiP^9`De={bQQ%heZC$QU2UpP(-tbl8 zPWD2abEew;oat@w`uP3J^YpsgT%~jT(Dk%oU}sa$7|n6hBjDj`+I;RX(>)%lm_7N{+B7Mu%H?422lE%MBJH!!YTN2oT7xr>>N-8OF$C&qU^ z>vLsa{$0X%q1fjOe3P1mCv#lN{xQ4_*HCSAZjTb1`}mlc+9rl8$B3OP%VT@mch_~G z7Y+4b{r>9e=M+7vSI;BgB?ryZDY4m>&wcHSn81VH1N~`0gvwH{ z8dv#hG|OK`>1;j7tM#B)Z7zDN?{6=dUal}$e ({ - prisma, -}); - -export type Context = inferAsyncReturnType; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated-router-helper.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated-router-helper.ts deleted file mode 100644 index c0226d129..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated-router-helper.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { router as createTRPCRouter } from '../trpc'; -export { publicProcedure as procedure } from '../trpc'; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/Post.nuxt.type.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/Post.nuxt.type.ts deleted file mode 100644 index 6f89f82c9..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/Post.nuxt.type.ts +++ /dev/null @@ -1,385 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { Prisma } from '@prisma/client'; -import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; -import type { MaybeRefOrGetter, UnwrapRef } from 'vue'; -import type { AsyncData, AsyncDataOptions } from 'nuxt/app'; -import type { KeysOf, PickFrom } from './utils'; -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; - -export interface ClientType { - aggregate: { - - query: (input: Prisma.Subset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - createMany: { - - mutate: (input?: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input?: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - create: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - deleteMany: { - - mutate: (input?: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input?: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - delete: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - findFirst: { - - query: (input?: Prisma.SelectSubset) => Promise | null>; - useQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findFirstOrThrow: { - - query: (input?: Prisma.SelectSubset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findMany: { - - query: (input?: Prisma.SelectSubset) => Promise>>; - useQuery: >, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: >, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findUnique: { - - query: (input: Prisma.SelectSubset) => Promise | null>; - useQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findUniqueOrThrow: { - - query: (input: Prisma.SelectSubset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - groupBy: { - - query: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >(input: Prisma.SubsetIntersection & InputErrors) => Promise<{} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors>; - useQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , ResT = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter & InputErrors>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , ResT = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter & InputErrors>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - updateMany: { - - mutate: (input: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - update: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - upsert: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - count: { - - query: (input?: Prisma.Subset) => Promise<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number>; - useQuery: - : number, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: - : number, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/User.nuxt.type.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/User.nuxt.type.ts deleted file mode 100644 index fd6e59da7..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/User.nuxt.type.ts +++ /dev/null @@ -1,385 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { Prisma } from '@prisma/client'; -import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; -import type { MaybeRefOrGetter, UnwrapRef } from 'vue'; -import type { AsyncData, AsyncDataOptions } from 'nuxt/app'; -import type { KeysOf, PickFrom } from './utils'; -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; - -export interface ClientType { - aggregate: { - - query: (input: Prisma.Subset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - createMany: { - - mutate: (input?: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input?: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - create: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - deleteMany: { - - mutate: (input?: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input?: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - delete: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - findFirst: { - - query: (input?: Prisma.SelectSubset) => Promise | null>; - useQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findFirstOrThrow: { - - query: (input?: Prisma.SelectSubset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findMany: { - - query: (input?: Prisma.SelectSubset) => Promise>>; - useQuery: >, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: >, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findUnique: { - - query: (input: Prisma.SelectSubset) => Promise | null>; - useQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: | null, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - findUniqueOrThrow: { - - query: (input: Prisma.SelectSubset) => Promise>; - useQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - groupBy: { - - query: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >(input: Prisma.SubsetIntersection & InputErrors) => Promise<{} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors>; - useQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , ResT = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter & InputErrors>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , ResT = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input: MaybeRefOrGetter & InputErrors>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; - updateMany: { - - mutate: (input: Prisma.SelectSubset) => Promise; - useMutation: , DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - update: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - upsert: { - - mutate: (input: Prisma.SelectSubset) => Promise>; - useMutation: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(opts?: Omit, 'lazy'> & { - trpc?: TRPCRequestOptions; - }) => AsyncData | DefaultT, DataE> & { - mutate: , DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf>(input: Prisma.SelectSubset) => Promise | null, DataE>['data']>>; - }; - - }; - count: { - - query: (input?: Prisma.Subset) => Promise<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number>; - useQuery: - : number, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - useLazyQuery: - : number, DataE = TRPCClientErrorLike, DataT = ResT, PickKeys extends KeysOf = KeysOf, DefaultT = null>(input?: MaybeRefOrGetter>, opts?: Omit, 'lazy' | 'watch'> & { - trpc?: TRPCRequestOptions; - queryKey?: string; - watch?: AsyncDataOptions['watch'] | false; - }) => AsyncData | DefaultT, DataE>; - - }; -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/nuxt.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/nuxt.ts deleted file mode 100644 index 1b1cba65c..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/nuxt.ts +++ /dev/null @@ -1,23 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; -import { createTRPCNuxtClient as _createTRPCNuxtClient } from 'trpc-nuxt/client'; -import type { DeepOverrideAtPath } from './utils'; -import { ClientType as UserClientType } from "./User.nuxt.type"; -import { ClientType as PostClientType } from "./Post.nuxt.type"; - -export function createTRPCNuxtClient( - opts: Parameters>[0] -) { - const r = _createTRPCNuxtClient(opts); - return r as DeepOverrideAtPath, TPath>; -} - -export interface ClientType { - user: UserClientType; - post: PostClientType; -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/utils.ts deleted file mode 100644 index 830996b8c..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/client/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -type Primitive = string | Function | number | boolean | Symbol | undefined | null; - -/** - * Recursively merges `T` and `R`. If there's a shared key, use `R`'s field type to overwrite `T`. - */ -export type DeepOverride = T extends Primitive - ? R - : R extends Primitive - ? R - : { - [K in keyof T]: K extends keyof R ? DeepOverride : T[K]; - } & { - [K in Exclude]: R[K]; - }; - -/** - * Traverse to `Path` (denoted by dot separated string literal type) in `T`, and starting from there, - * recursively merge with `R`. - */ -export type DeepOverrideAtPath = Path extends undefined - ? DeepOverride - : Path extends `${infer P1}.${infer P2}` - ? P1 extends keyof T - ? Omit & Record>> - : never - : Path extends keyof T - ? Omit & Record> - : never; - -// Utility type from 'trpc-nuxt' -export type KeysOf = Array; - -// Utility type from 'trpc-nuxt' -export type PickFrom> = T extends Array - ? T - : T extends Record - ? keyof T extends K[number] - ? T - : K[number] extends never - ? T - : Pick - : T; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/helper.ts deleted file mode 100644 index 183476950..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/helper.ts +++ /dev/null @@ -1,74 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { TRPCError } from '@trpc/server'; -import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; - -export async function checkMutate(promise: Promise): Promise { - try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - if (err.meta?.reason === 'RESULT_NOT_READABLE') { - // unable to readback data - return undefined; - } else { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }); - } - } else { - throw err; - } - } - -} - -export async function checkRead(promise: Promise): Promise { - try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } else if (err.code === 'P2025') { - // not found - throw new TRPCError({ - code: 'NOT_FOUND', - message: err.message, - cause: err, - }); - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }) - } - } else { - throw err; - } - } - -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/Post.router.ts deleted file mode 100644 index ae11381f9..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/Post.router.ts +++ /dev/null @@ -1,49 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { db } from "."; -import { createTRPCRouter } from "../../generated-router-helper"; -import { procedure } from "../../generated-router-helper"; -import * as _Schema from '@zenstackhq/runtime/zod/input'; -const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; -import { checkRead, checkMutate } from '../helper'; - -export default function createRouter() { - return createTRPCRouter({ - - aggregate: procedure.input($Schema.PostInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).post.aggregate(input as any))), - - createMany: procedure.input($Schema.PostInputSchema.createMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.createMany(input as any))), - - create: procedure.input($Schema.PostInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.create(input as any))), - - deleteMany: procedure.input($Schema.PostInputSchema.deleteMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.deleteMany(input as any))), - - delete: procedure.input($Schema.PostInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.delete(input as any))), - - findFirst: procedure.input($Schema.PostInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findFirst(input as any))), - - findFirstOrThrow: procedure.input($Schema.PostInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findFirstOrThrow(input as any))), - - findMany: procedure.input($Schema.PostInputSchema.findMany.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findMany(input as any))), - - findUnique: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUnique(input as any))), - - findUniqueOrThrow: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUniqueOrThrow(input as any))), - - groupBy: procedure.input($Schema.PostInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).post.groupBy(input as any))), - - updateMany: procedure.input($Schema.PostInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.updateMany(input as any))), - - update: procedure.input($Schema.PostInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.update(input as any))), - - upsert: procedure.input($Schema.PostInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.upsert(input as any))), - - count: procedure.input($Schema.PostInputSchema.count.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.count(input as any))), - - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/User.router.ts deleted file mode 100644 index 7e81f0052..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/User.router.ts +++ /dev/null @@ -1,49 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { db } from "."; -import { createTRPCRouter } from "../../generated-router-helper"; -import { procedure } from "../../generated-router-helper"; -import * as _Schema from '@zenstackhq/runtime/zod/input'; -const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; -import { checkRead, checkMutate } from '../helper'; - -export default function createRouter() { - return createTRPCRouter({ - - aggregate: procedure.input($Schema.UserInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).user.aggregate(input as any))), - - createMany: procedure.input($Schema.UserInputSchema.createMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.createMany(input as any))), - - create: procedure.input($Schema.UserInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.create(input as any))), - - deleteMany: procedure.input($Schema.UserInputSchema.deleteMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.deleteMany(input as any))), - - delete: procedure.input($Schema.UserInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.delete(input as any))), - - findFirst: procedure.input($Schema.UserInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findFirst(input as any))), - - findFirstOrThrow: procedure.input($Schema.UserInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findFirstOrThrow(input as any))), - - findMany: procedure.input($Schema.UserInputSchema.findMany.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findMany(input as any))), - - findUnique: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUnique(input as any))), - - findUniqueOrThrow: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUniqueOrThrow(input as any))), - - groupBy: procedure.input($Schema.UserInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).user.groupBy(input as any))), - - updateMany: procedure.input($Schema.UserInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.updateMany(input as any))), - - update: procedure.input($Schema.UserInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.update(input as any))), - - upsert: procedure.input($Schema.UserInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.upsert(input as any))), - - count: procedure.input($Schema.UserInputSchema.count.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.count(input as any))), - - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/index.ts deleted file mode 100644 index a71da919e..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/generated/routers/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { AnyTRPCRouter as AnyRouter } from "@trpc/server"; -import type { PrismaClient } from "@prisma/client"; -import { createTRPCRouter } from "../../generated-router-helper"; -import createUserRouter from "./User.router"; -import createPostRouter from "./Post.router"; - -export function db(ctx: any) { - if (!ctx.prisma) { - throw new Error('Missing "prisma" field in trpc context'); - } - return ctx.prisma as PrismaClient; -} - -export function createRouter() { - return createTRPCRouter({ - user: createUserRouter(), - post: createPostRouter(), - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/index.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/index.ts deleted file mode 100644 index 731355810..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/routers/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createRouter as createCRUDRouter } from './generated/routers'; - -export const appRouter = createCRUDRouter(); - -// export type definition of API -export type AppRouter = typeof appRouter; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/trpc.ts b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/trpc.ts deleted file mode 100644 index 0b93ecda7..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/trpc/trpc.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This is your entry point to setup the root configuration for tRPC on the server. - * - `initTRPC` should only be used once per app. - * - We export only the functionality that we use so we can enforce which base procedures should be used - * - * Learn how to create protected base procedures and other things below: - * @see https://trpc.io/docs/v10/router - * @see https://trpc.io/docs/v10/procedures - */ -import { initTRPC } from '@trpc/server'; -import { Context } from './context'; - -const t = initTRPC.context().create(); - -/** - * Unprotected procedure - **/ -export const publicProcedure = t.procedure; -export const router = t.router; -export const middleware = t.middleware; diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/tsconfig.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/tsconfig.json deleted file mode 100644 index b9ed69c19..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/server/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../.nuxt/tsconfig.server.json" -} diff --git a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/tsconfig.json b/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/tsconfig.json deleted file mode 100644 index f80f43a36..000000000 --- a/packages/plugins/trpc/tests/projects/nuxt-trpc-v11/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - // https://nuxt.com/docs/guide/concepts/typescript - "extends": "./.nuxt/tsconfig.json", - "compilerOptions": { - "verbatimModuleSyntax": false - } -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/.eslintrc.cjs b/packages/plugins/trpc/tests/projects/t3-trpc-v10/.eslintrc.cjs deleted file mode 100644 index c946e46e0..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/.eslintrc.cjs +++ /dev/null @@ -1,37 +0,0 @@ -/** @type {import("eslint").Linter.Config} */ -const config = { - parser: "@typescript-eslint/parser", - parserOptions: { - project: true, - }, - plugins: ["@typescript-eslint"], - extends: [ - "next/core-web-vitals", - "plugin:@typescript-eslint/recommended-type-checked", - "plugin:@typescript-eslint/stylistic-type-checked", - ], - rules: { - // These opinionated rules are enabled in stylistic-type-checked above. - // Feel free to reconfigure them to your own preference. - "@typescript-eslint/array-type": "off", - "@typescript-eslint/consistent-type-definitions": "off", - - "@typescript-eslint/consistent-type-imports": [ - "warn", - { - prefer: "type-imports", - fixStyle: "inline-type-imports", - }, - ], - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - "@typescript-eslint/require-await": "off", - "@typescript-eslint/no-misused-promises": [ - "error", - { - checksVoidReturn: { attributes: false }, - }, - ], - }, -}; - -module.exports = config; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/.gitignore b/packages/plugins/trpc/tests/projects/t3-trpc-v10/.gitignore deleted file mode 100644 index e3f5e79f8..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/.gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# database -/prisma/db.sqlite -/prisma/db.sqlite-journal - -# next.js -/.next/ -/out/ -next-env.d.ts - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables -.env -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -package-lock.json -package.json \ No newline at end of file diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/README.md b/packages/plugins/trpc/tests/projects/t3-trpc-v10/README.md deleted file mode 100644 index fba19edac..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Create T3 App - -This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. - -## What's next? How do I make an app with this? - -We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. - -If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. - -- [Next.js](https://nextjs.org) -- [NextAuth.js](https://next-auth.js.org) -- [Prisma](https://prisma.io) -- [Tailwind CSS](https://tailwindcss.com) -- [tRPC](https://trpc.io) - -## Learn More - -To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: - -- [Documentation](https://create.t3.gg/) -- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials - -You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! - -## How do I deploy this? - -Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/next.config.js b/packages/plugins/trpc/tests/projects/t3-trpc-v10/next.config.js deleted file mode 100644 index ffbeb9fb4..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/next.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful - * for Docker builds. - */ -await import("./src/env.js"); - -/** @type {import("next").NextConfig} */ -const config = { - reactStrictMode: true, - - /** - * If you are using `appDir` then you must comment the below `i18n` config out. - * - * @see https://github.com/vercel/next.js/issues/41980 - */ - i18n: { - locales: ["en"], - defaultLocale: "en", - }, -}; - -export default config; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json b/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json deleted file mode 100644 index 5f6990487..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "t3-trpc-v10", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "build": "next build", - "db:push": "prisma db push", - "db:studio": "prisma studio", - "dev": "next dev", - "postinstall": "prisma generate", - "lint": "next lint", - "start": "next start" - }, - "dependencies": { - "@prisma/client": "6.0.x", - "@t3-oss/env-nextjs": "^0.13.1", - "@tanstack/react-query": "^4.36.1", - "@trpc/client": "^10.43.6", - "@trpc/next": "^10.43.6", - "@trpc/react-query": "^10.43.6", - "@trpc/server": "^10.43.6", - "next": "^14.0.4", - "react": "18.2.0", - "react-dom": "18.2.0", - "superjson": "^2.2.1", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/eslint": "^8.44.7", - "@types/node": "^18.17.0", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", - "eslint": "^8.54.0", - "eslint-config-next": "^14.0.4", - "prisma": "6.0.x", - "typescript": "^5.5.4" - }, - "ct3aMetadata": { - "initVersion": "7.26.0" - }, - "packageManager": "npm@10.2.3" -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/prisma/schema.prisma b/packages/plugins/trpc/tests/projects/t3-trpc-v10/prisma/schema.prisma deleted file mode 100644 index a28fea9fb..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/prisma/schema.prisma +++ /dev/null @@ -1,31 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -////////////////////////////////////////////////////////////////////////////////////////////// - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -generator client { - provider = "prisma-client-js" -} - -model User { - id Int @id() @default(autoincrement()) - email String @unique() - posts Post[] -} - -model Post { - id Int @id() @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int - - @@index([name]) -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/public/favicon.ico b/packages/plugins/trpc/tests/projects/t3-trpc-v10/public/favicon.ico deleted file mode 100644 index 60c702aac13409c82c4040f8fee9603eb84aa10c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 zcmeHOX^b4j6>hH&u3!wtgoMM(Da<7x5Rn8Tjvq)?zybua1c*c20{$U^Aj%>HD9R-z zBuWB;ARB>eYK+y}Dk#s*$8Q(p+iLA_;N7bn84x`l%#I{rywlCmbkAPayBK)27Rhm!$WXNYV+Q zK^4@P%189Q__>DsD^FL?7r_P=JJvIlKl+CJ62dyd9WqHP5Sp7xG3? zPX~|xApI@EB+@r<8Zj2@M^QA-H<*IF*CS2am*|i;*E8hDd|es0ZRN*eT}q4f={qph zUyoWUdZ+R8ip52QA+%;l|Sa2^7!PrYAH1GAw4nmfKx zy1+M;Td^MB<(cfVs$m?`u57IHH)D=|NWm@3zi7tCFgENLSn7k=Pdv~@u`r2!VJ-Hn z5cXiS66w9>LT56iNCfU;)=ma= z`c9MYdEO$lXDjiAZma0~qmy`0Ud=wxm3KG>+B=)kiuq~siOx6F`)WK*<@aK}q_nFk z=W_Xo|D8k=&&!fe^n@YJ_ToIDgFft$u(^~7d^hv_v^bCawEFQf^jDeWqrcR6S<-hm zzt3;Z#bYA(s$u7s3+5#DVP-&b)99=$<^0;j3 zcc)NTm?l#!%OgJ;80Z7t%UlN9>UibS>6}kssAr=rpgqWS-2-@jo;Z(u;uA5p57xgo zI0neFT|+sU%cxe{+k^AUuVIL^eX;Kh)-j;ZL#;@rXxqP5TdFpnH#&uyA7>w*DZVL^MIqKm_{4qbT z46X9@-3V1(K8eflW%)qJ|3tvB*;bOy=By;paJ=otl~IG8!ZC!Zx){7a=ln4@zlz(V z7_;4!AJJMDRWX`AY2VT|#s$X@PdzK-T;Cwj zne8F?PcC3MKk<6qh;i;b3A`O4yiT@P9^OMkLknOd-T*uDYt$b@NVA1^;H+n%Evu7k z>pb$3Xk0dOYE15jHpV~_t(ZqPKm3n>Lf!4L`e|*bmEqhbhbHxN@*R{YCoA0!{t9D< z0rS(%aWflbbWbUxZ)$$W8ML~>yt1+aZJ3*dF|E8+{1N%xP5HMN-`hk?7#G{oA8!yQ zItP22wv^7IzFj^8bPprM{pCAx9^42%$E4xQDr*&g=#+mBoDDzCl#kAa&+K;Sa(**; z)E3lx6Km5h?MAzv?PMIaf}i>te(+aCy+em3%;8I#;TH2vtP6w}Vahi-HQy%#tysSg z73uS&TftwgXv=7vaQstao88lj{;Ha`Y{og-R9*p(zC3v2G_ByLx>&>Sg}!UPZM37{ z>Basy&#Z6gV1VnO7GpiK)nUBcX#LkJemal)mUNRv0cJMfcj3f=Dz^j|@H+FCaH(7$6r#>64S<{vsG@JOVy1oSK4xI(+V+VV|j!MdVwyb&3DSn!Z zW3G1OR$D#3*?L50mMZRep!apV>Ydsl|2+$1T6rh6M@FW$H2ME+kz+>T7Wl^_o9vCD6fxs zw5dex9l)Jn7RI#lcJW8_p3YR>!}x930X2N`de0kKO7H%-T=dC&PcQue_TD(`)d{Ti zpVECPFYhF77eC3Qa|(3&+O+N)x;5nYT$7zD;-a%M-T>Z>_WovzZD*cO#ky(fPVm!M z4>50XE?F;*jp_Cb#^0xcejdVtG(4@Ab%LME8grb(VULnMQ(qTr?XlS4sA=Z%WpG}t z#@)bAGHE<}Ce}x+=VD)Aj=U1KY1`*%OSkaTSaNZniT#y)LG`(S^d#o(f0N$S=E0Xm z69nua-)1-RfN`**lRLwjZKf-mfScCTMmtQlmSkn&*%Qh=t8_ZBe~{3IHMCdn2^iBb zU@Z0dnsO%gtl?N6Y{&Y!ir&Ac*2mk5ac|mxL_VZh2;?)RIUwSqJpgNKaYjEF@;@WI zV;5<~G|ty}hr~8c`6=kme>Q^rmKXb<0b#1W2{PGdGuxm%Zdt`cMch0MyXbn%Lwe)X zm_Ofrn*7V_#@MFAI1Y+wEV-6^4ls%5b>Nb>VQu|ugs~#hQ+hYypVk$7do)3>4&JN5 z*Qv&IioJq8V&L9GYy;NYm7v3!Od*?f)&p$Ie z=7xjyDDv&@jzB)Sqc--SY9FM2yJ2IM#O`-=V2OZPO;(?CxHJq`3Uu%~L^f2g^2 A#Q*>R diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/schema.zmodel b/packages/plugins/trpc/tests/projects/t3-trpc-v10/schema.zmodel deleted file mode 100644 index 01c23bd80..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/schema.zmodel +++ /dev/null @@ -1,35 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -plugin trpc { - provider = "../../../dist" - output = "src/server/api/routers/generated" - generateClientHelpers = "next" -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int - - @@index([name]) -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/env.js b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/env.js deleted file mode 100644 index d0ba1eb56..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/env.js +++ /dev/null @@ -1,40 +0,0 @@ -import { createEnv } from '@t3-oss/env-nextjs'; -import { z } from 'zod'; - -export const env = createEnv({ - /** - * Specify your server-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. - */ - server: { - NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), - }, - - /** - * Specify your client-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. To expose them to the client, prefix them with - * `NEXT_PUBLIC_`. - */ - client: { - // NEXT_PUBLIC_CLIENTVAR: z.string(), - }, - - /** - * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. - * middlewares) or client-side so we need to destruct manually. - */ - runtimeEnv: { - NODE_ENV: process.env.NODE_ENV, - // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, - }, - /** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially - * useful for Docker builds. - */ - skipValidation: !!process.env.SKIP_ENV_VALIDATION, - /** - * Makes it so that empty strings are treated as undefined. - * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error. - */ - emptyStringAsUndefined: true, -}); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/_app.tsx b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/_app.tsx deleted file mode 100644 index 18319174e..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/_app.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { type AppType } from "next/app"; - -import { api } from "~/utils/api"; - -import "~/styles/globals.css"; - -const MyApp: AppType = ({ Component, pageProps }) => { - return ; -}; - -export default api.withTRPC(MyApp); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/api/trpc/[trpc].ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/api/trpc/[trpc].ts deleted file mode 100644 index 587dd2bdf..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/api/trpc/[trpc].ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createNextApiHandler } from "@trpc/server/adapters/next"; - -import { env } from "~/env"; -import { appRouter } from "~/server/api/root"; -import { createTRPCContext } from "~/server/api/trpc"; - -// export API handler -export default createNextApiHandler({ - router: appRouter, - createContext: createTRPCContext, - onError: - env.NODE_ENV === "development" - ? ({ path, error }) => { - console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}` - ); - } - : undefined, -}); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/index.module.css b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/index.module.css deleted file mode 100644 index fac9982a3..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/index.module.css +++ /dev/null @@ -1,177 +0,0 @@ -.main { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 100vh; - background-image: linear-gradient(to bottom, #2e026d, #15162c); -} - -.container { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 3rem; - padding: 4rem 1rem; -} - -@media (min-width: 640px) { - .container { - max-width: 640px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 768px; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 1024px; - } -} - -@media (min-width: 1280px) { - .container { - max-width: 1280px; - } -} - -@media (min-width: 1536px) { - .container { - max-width: 1536px; - } -} - -.title { - font-size: 3rem; - line-height: 1; - font-weight: 800; - letter-spacing: -0.025em; - margin: 0; - color: white; -} - -@media (min-width: 640px) { - .title { - font-size: 5rem; - } -} - -.pinkSpan { - color: hsl(280 100% 70%); -} - -.cardRow { - display: grid; - grid-template-columns: repeat(1, minmax(0, 1fr)); - gap: 1rem; -} - -@media (min-width: 640px) { - .cardRow { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (min-width: 768px) { - .cardRow { - gap: 2rem; - } -} - -.card { - max-width: 20rem; - display: flex; - flex-direction: column; - gap: 1rem; - padding: 1rem; - border-radius: 0.75rem; - color: white; - background-color: rgb(255 255 255 / 0.1); -} - -.card:hover { - background-color: rgb(255 255 255 / 0.2); - transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); -} - -.cardTitle { - font-size: 1.5rem; - line-height: 2rem; - font-weight: 700; - margin: 0; -} - -.cardText { - font-size: 1.125rem; - line-height: 1.75rem; -} - -.showcaseContainer { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; -} - -.showcaseText { - color: white; - text-align: center; - font-size: 1.5rem; - line-height: 2rem; -} - -.authContainer { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; -} - -.loginButton { - border-radius: 9999px; - background-color: rgb(255 255 255 / 0.1); - padding: 0.75rem 2.5rem; - font-weight: 600; - color: white; - text-decoration-line: none; - transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); -} - -.loginButton:hover { - background-color: rgb(255 255 255 / 0.2); -} - -.form { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.input { - width: 100%; - border-radius: 9999px; - padding: 0.5rem 1rem; - color: black; -} - -.submitButton { - all: unset; - border-radius: 9999px; - background-color: rgb(255 255 255 / 0.1); - padding: 0.75rem 2.5rem; - font-weight: 600; - color: white; - text-align: center; - transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); -} - -.submitButton:hover { - background-color: rgb(255 255 255 / 0.2); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/index.tsx b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/index.tsx deleted file mode 100644 index e8e910ced..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/pages/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { api } from '~/utils/api'; -import styles from './index.module.css'; - -export default function Home() { - const hello = api.greet.hello.useQuery({ text: 'from tRPC' }); - const posts = api.post.post.findMany.useQuery({ where: { published: true }, include: { author: true } }); - const postsTransformed = api.post.post.findMany.useQuery( - {}, - { select: (data) => data.map((p) => ({ id: p.id, title: p.name })) } - ); - - const { mutateAsync: createPost } = api.post.post.create.useMutation(); - - const mutation = async () => { - const created = await createPost({ - data: { name: 'New post', published: true, authorId: 1 }, - include: { author: true }, - }); - console.log(created.author.email); - }; - - return ( - <> -
- {hello.data &&

{hello.data.greeting}

} - {posts.data?.map((post) => ( -

- {post.name} by {post.author.email} -

- ))} - {postsTransformed.data?.map((post) => ( -

{post.title}

- ))} - - -
- - ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/root.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/root.ts deleted file mode 100644 index 397c562bf..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/root.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { greetRouter } from '~/server/api/routers/greet'; -import { createTRPCRouter } from '~/server/api/trpc'; -import { postRouter } from './routers/post'; - -/** - * This is the primary router for your server. - * - * All routers added in /api/routers should be manually added here. - */ -export const appRouter = createTRPCRouter({ - greet: greetRouter, - post: postRouter, -}); - -// export type definition of API -export type AppRouter = typeof appRouter; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/Post.next.type.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/Post.next.type.ts deleted file mode 100644 index 58a2222ad..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/Post.next.type.ts +++ /dev/null @@ -1,383 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { Prisma } from '@prisma/client'; -import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; -import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; -import type { AnyRouter } from '@trpc/server'; - -export interface ClientType { - aggregate: { - - useQuery: >( - input: Prisma.Subset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.GetPostAggregateType, - TRPCClientErrorLike - >; - - }; - createMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostCreateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables?: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - create: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostCreateArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> - }; - - }; - deleteMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostDeleteManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables?: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - delete: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostDeleteArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> - }; - - }; - findFirst: { - - useQuery: | null>( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions | null, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions | null, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.PostGetPayload | null, - TRPCClientErrorLike - >; - - }; - findFirstOrThrow: { - - useQuery: >( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.PostGetPayload, - TRPCClientErrorLike - >; - - }; - findMany: { - - useQuery: >>( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions>, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions>, Error> - ) => UseTRPCInfiniteQueryResult< - Array>, - TRPCClientErrorLike - >; - - }; - findUnique: { - - useQuery: | null>( - input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions | null, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions | null, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.PostGetPayload | null, - TRPCClientErrorLike - >; - - }; - findUniqueOrThrow: { - - useQuery: >( - input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.PostGetPayload, - TRPCClientErrorLike - >; - - }; - groupBy: { - - useQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , TData = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors>( - input: Prisma.SubsetIntersection & InputErrors, - opts?: UseTRPCQueryOptions : InputErrors, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >( - input: Omit & InputErrors, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions : InputErrors, Error> - ) => UseTRPCInfiniteQueryResult< - {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, - TRPCClientErrorLike - >; - - }; - updateMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostUpdateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - update: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostUpdateArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> - }; - - }; - upsert: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostUpsertArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> - }; - - }; - count: { - - useQuery: - : number>( - input?: Prisma.Subset, - opts?: UseTRPCQueryOptions - : number, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions - : number, Error> - ) => UseTRPCInfiniteQueryResult< - 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, - TRPCClientErrorLike - >; - - }; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/User.next.type.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/User.next.type.ts deleted file mode 100644 index 10f654d50..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/User.next.type.ts +++ /dev/null @@ -1,383 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { Prisma } from '@prisma/client'; -import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; -import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; -import type { AnyRouter } from '@trpc/server'; - -export interface ClientType { - aggregate: { - - useQuery: >( - input: Prisma.Subset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.GetUserAggregateType, - TRPCClientErrorLike - >; - - }; - createMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserCreateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables?: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - create: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserCreateArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> - }; - - }; - deleteMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserDeleteManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables?: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - delete: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserDeleteArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> - }; - - }; - findFirst: { - - useQuery: | null>( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions | null, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions | null, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.UserGetPayload | null, - TRPCClientErrorLike - >; - - }; - findFirstOrThrow: { - - useQuery: >( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.UserGetPayload, - TRPCClientErrorLike - >; - - }; - findMany: { - - useQuery: >>( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions>, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions>, Error> - ) => UseTRPCInfiniteQueryResult< - Array>, - TRPCClientErrorLike - >; - - }; - findUnique: { - - useQuery: | null>( - input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions | null, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions | null, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.UserGetPayload | null, - TRPCClientErrorLike - >; - - }; - findUniqueOrThrow: { - - useQuery: >( - input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.UserGetPayload, - TRPCClientErrorLike - >; - - }; - groupBy: { - - useQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , TData = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors>( - input: Prisma.SubsetIntersection & InputErrors, - opts?: UseTRPCQueryOptions : InputErrors, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >( - input: Omit & InputErrors, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions : InputErrors, Error> - ) => UseTRPCInfiniteQueryResult< - {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, - TRPCClientErrorLike - >; - - }; - updateMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserUpdateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - update: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserUpdateArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> - }; - - }; - upsert: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserUpsertArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> - }; - - }; - count: { - - useQuery: - : number>( - input?: Prisma.Subset, - opts?: UseTRPCQueryOptions - : number, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions - : number, Error> - ) => UseTRPCInfiniteQueryResult< - 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, - TRPCClientErrorLike - >; - - }; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts deleted file mode 100644 index b7983ab73..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/next.ts +++ /dev/null @@ -1,27 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { AnyRouter } from '@trpc/server'; -import type { NextPageContext } from 'next'; -import { type CreateTRPCNext, createTRPCNext as _createTRPCNext } from '@trpc/next'; -import type { DeepOverrideAtPath } from './utils'; -import { ClientType as UserClientType } from "./User.next.type"; -import { ClientType as PostClientType } from "./Post.next.type"; - -export function createTRPCNext< - TRouter extends AnyRouter, - TPath extends string | undefined = undefined, - TSSRContext extends NextPageContext = NextPageContext, - TFlags = null ->(opts: Parameters[0]) { - const r: CreateTRPCNext = _createTRPCNext(opts); - return r as DeepOverrideAtPath, ClientType, TPath>; -} - -export interface ClientType { - user: UserClientType; - post: PostClientType; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts deleted file mode 100644 index 830996b8c..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/client/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -type Primitive = string | Function | number | boolean | Symbol | undefined | null; - -/** - * Recursively merges `T` and `R`. If there's a shared key, use `R`'s field type to overwrite `T`. - */ -export type DeepOverride = T extends Primitive - ? R - : R extends Primitive - ? R - : { - [K in keyof T]: K extends keyof R ? DeepOverride : T[K]; - } & { - [K in Exclude]: R[K]; - }; - -/** - * Traverse to `Path` (denoted by dot separated string literal type) in `T`, and starting from there, - * recursively merge with `R`. - */ -export type DeepOverrideAtPath = Path extends undefined - ? DeepOverride - : Path extends `${infer P1}.${infer P2}` - ? P1 extends keyof T - ? Omit & Record>> - : never - : Path extends keyof T - ? Omit & Record> - : never; - -// Utility type from 'trpc-nuxt' -export type KeysOf = Array; - -// Utility type from 'trpc-nuxt' -export type PickFrom> = T extends Array - ? T - : T extends Record - ? keyof T extends K[number] - ? T - : K[number] extends never - ? T - : Pick - : T; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts deleted file mode 100644 index 183476950..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/helper.ts +++ /dev/null @@ -1,74 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { TRPCError } from '@trpc/server'; -import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; - -export async function checkMutate(promise: Promise): Promise { - try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - if (err.meta?.reason === 'RESULT_NOT_READABLE') { - // unable to readback data - return undefined; - } else { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }); - } - } else { - throw err; - } - } - -} - -export async function checkRead(promise: Promise): Promise { - try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } else if (err.code === 'P2025') { - // not found - throw new TRPCError({ - code: 'NOT_FOUND', - message: err.message, - cause: err, - }); - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }) - } - } else { - throw err; - } - } - -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts deleted file mode 100644 index f3cf556bf..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/Post.router.ts +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; -import * as _Schema from '@zenstackhq/runtime/zod/input'; -const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; -import { checkRead, checkMutate } from '../helper'; - -export default function createRouter(router: RouterFactory, procedure: ProcBuilder) { - return router({ - - aggregate: procedure.input($Schema.PostInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).post.aggregate(input as any))), - - createMany: procedure.input($Schema.PostInputSchema.createMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.createMany(input as any))), - - create: procedure.input($Schema.PostInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.create(input as any))), - - deleteMany: procedure.input($Schema.PostInputSchema.deleteMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.deleteMany(input as any))), - - delete: procedure.input($Schema.PostInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.delete(input as any))), - - findFirst: procedure.input($Schema.PostInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findFirst(input as any))), - - findFirstOrThrow: procedure.input($Schema.PostInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findFirstOrThrow(input as any))), - - findMany: procedure.input($Schema.PostInputSchema.findMany.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findMany(input as any))), - - findUnique: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUnique(input as any))), - - findUniqueOrThrow: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUniqueOrThrow(input as any))), - - groupBy: procedure.input($Schema.PostInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).post.groupBy(input as any))), - - updateMany: procedure.input($Schema.PostInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.updateMany(input as any))), - - update: procedure.input($Schema.PostInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.update(input as any))), - - upsert: procedure.input($Schema.PostInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.upsert(input as any))), - - count: procedure.input($Schema.PostInputSchema.count.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.count(input as any))), - - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts deleted file mode 100644 index a7d04017e..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/User.router.ts +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { type RouterFactory, type ProcBuilder, type BaseConfig, db } from "."; -import * as _Schema from '@zenstackhq/runtime/zod/input'; -const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; -import { checkRead, checkMutate } from '../helper'; - -export default function createRouter(router: RouterFactory, procedure: ProcBuilder) { - return router({ - - aggregate: procedure.input($Schema.UserInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).user.aggregate(input as any))), - - createMany: procedure.input($Schema.UserInputSchema.createMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.createMany(input as any))), - - create: procedure.input($Schema.UserInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.create(input as any))), - - deleteMany: procedure.input($Schema.UserInputSchema.deleteMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.deleteMany(input as any))), - - delete: procedure.input($Schema.UserInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.delete(input as any))), - - findFirst: procedure.input($Schema.UserInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findFirst(input as any))), - - findFirstOrThrow: procedure.input($Schema.UserInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findFirstOrThrow(input as any))), - - findMany: procedure.input($Schema.UserInputSchema.findMany.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findMany(input as any))), - - findUnique: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUnique(input as any))), - - findUniqueOrThrow: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUniqueOrThrow(input as any))), - - groupBy: procedure.input($Schema.UserInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).user.groupBy(input as any))), - - updateMany: procedure.input($Schema.UserInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.updateMany(input as any))), - - update: procedure.input($Schema.UserInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.update(input as any))), - - upsert: procedure.input($Schema.UserInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.upsert(input as any))), - - count: procedure.input($Schema.UserInputSchema.count.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.count(input as any))), - - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts deleted file mode 100644 index 2925db282..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/generated/routers/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { unsetMarker, AnyRouter, AnyRootConfig, CreateRouterInner, Procedure, ProcedureBuilder, ProcedureParams, ProcedureRouterRecord, ProcedureType } from "@trpc/server"; -import type { PrismaClient } from "@prisma/client"; -import createUserRouter from "./User.router"; -import createPostRouter from "./Post.router"; - -export type BaseConfig = AnyRootConfig; - -export type RouterFactory = < - ProcRouterRecord extends ProcedureRouterRecord ->( - procedures: ProcRouterRecord -) => CreateRouterInner; - -export type UnsetMarker = typeof unsetMarker; - -export type ProcBuilder = ProcedureBuilder< - ProcedureParams ->; - -export function db(ctx: any) { - if (!ctx.prisma) { - throw new Error('Missing "prisma" field in trpc context'); - } - return ctx.prisma as PrismaClient; -} - -export function createRouter(router: RouterFactory, procedure: ProcBuilder) { - return router({ - user: createUserRouter(router, procedure), - post: createPostRouter(router, procedure), - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/greet.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/greet.ts deleted file mode 100644 index 2d1fbbbea..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/greet.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod'; - -import { createTRPCRouter, publicProcedure } from '~/server/api/trpc'; - -export const greetRouter = createTRPCRouter({ - hello: publicProcedure.input(z.object({ text: z.string() })).query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), -}); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/post.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/post.ts deleted file mode 100644 index 04067d65c..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/routers/post.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createTRPCRouter, publicProcedure } from '../trpc'; -import { createRouter } from './generated/routers'; - -export const postRouter = createRouter(createTRPCRouter, publicProcedure); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/trpc.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/trpc.ts deleted file mode 100644 index dc3b40b24..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/api/trpc.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC } from "@trpc/server"; -import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; -import superjson from "superjson"; -import { ZodError } from "zod"; - -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -type CreateContextOptions = Record; - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = (_opts: CreateContextOptions) => { - return { - db, - }; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = (_opts: CreateNextContextOptions) => { - return createInnerTRPCContext({}); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/db.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/db.ts deleted file mode 100644 index 02696bcc3..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/server/db.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -import { env } from "~/env"; - -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; -}; - -export const db = - globalForPrisma.prisma ?? - new PrismaClient({ - log: - env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], - }); - -if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/styles/globals.css b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/styles/globals.css deleted file mode 100644 index e5e2dcc23..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/styles/globals.css +++ /dev/null @@ -1,16 +0,0 @@ -html, -body { - padding: 0; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} - -a { - color: inherit; - text-decoration: none; -} - -* { - box-sizing: border-box; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/utils/api.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/utils/api.ts deleted file mode 100644 index 53905ef28..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/src/utils/api.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which - * contains the Next.js App-wrapper, as well as your type-safe React Query hooks. - * - * We also create a few inference helpers for input and output types. - */ -import { httpBatchLink, loggerLink } from '@trpc/client'; -import { createTRPCNext } from '~/server/api/routers/generated/client/next'; -import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; -import superjson from 'superjson'; - -import { type AppRouter } from '~/server/api/root'; - -const getBaseUrl = () => { - if (typeof window !== 'undefined') return ''; // browser should use relative url - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url - return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost -}; - -/** A set of type-safe react-query hooks for your tRPC API. */ -export const api = createTRPCNext({ - config() { - return { - /** - * Transformer used for data de-serialization from the server. - * - * @see https://trpc.io/docs/data-transformers - */ - transformer: superjson, - - /** - * Links used to determine request flow from client to server. - * - * @see https://trpc.io/docs/links - */ - links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === 'development' || - (opts.direction === 'down' && opts.result instanceof Error), - }), - httpBatchLink({ - url: `${getBaseUrl()}/api/trpc`, - }), - ], - }; - }, - /** - * Whether tRPC should await queries when server rendering pages. - * - * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false - */ - ssr: false, -}); - -/** - * Inference helper for inputs. - * - * @example type HelloInput = RouterInputs['example']['hello'] - */ -export type RouterInputs = inferRouterInputs; - -/** - * Inference helper for outputs. - * - * @example type HelloOutput = RouterOutputs['example']['hello'] - */ -export type RouterOutputs = inferRouterOutputs; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v10/tsconfig.json b/packages/plugins/trpc/tests/projects/t3-trpc-v10/tsconfig.json deleted file mode 100644 index 905062ded..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v10/tsconfig.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "compilerOptions": { - /* Base Options: */ - "esModuleInterop": true, - "skipLibCheck": true, - "target": "es2022", - "allowJs": true, - "resolveJsonModule": true, - "moduleDetection": "force", - "isolatedModules": true, - - /* Strictness */ - "strict": true, - "noUncheckedIndexedAccess": true, - "checkJs": true, - - /* Bundled projects */ - "lib": ["dom", "dom.iterable", "ES2022"], - "noEmit": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "jsx": "preserve", - "plugins": [{ "name": "next" }], - "incremental": true, - - /* Path Aliases */ - "baseUrl": ".", - "paths": { - "~/*": ["./src/*"] - } - }, - "include": [ - ".eslintrc.cjs", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.cjs", - "**/*.js", - ".next/types/**/*.ts" - ], - "exclude": ["node_modules"] -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/.eslintrc.cjs b/packages/plugins/trpc/tests/projects/t3-trpc-v11/.eslintrc.cjs deleted file mode 100644 index a92fb0bc2..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/.eslintrc.cjs +++ /dev/null @@ -1,42 +0,0 @@ -/** @type {import("eslint").Linter.Config} */ -const config = { - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": true - }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "next/core-web-vitals", - "plugin:@typescript-eslint/recommended-type-checked", - "plugin:@typescript-eslint/stylistic-type-checked" - ], - "rules": { - "@typescript-eslint/array-type": "off", - "@typescript-eslint/consistent-type-definitions": "off", - "@typescript-eslint/consistent-type-imports": [ - "warn", - { - "prefer": "type-imports", - "fixStyle": "inline-type-imports" - } - ], - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_" - } - ], - "@typescript-eslint/require-await": "off", - "@typescript-eslint/no-misused-promises": [ - "error", - { - "checksVoidReturn": { - "attributes": false - } - } - ] - } -} -module.exports = config; \ No newline at end of file diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/.gitignore b/packages/plugins/trpc/tests/projects/t3-trpc-v11/.gitignore deleted file mode 100644 index c24a8359c..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# database -/prisma/db.sqlite -/prisma/db.sqlite-journal -db.sqlite - -# next.js -/.next/ -/out/ -next-env.d.ts - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables -.env -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo - -# idea files -.idea \ No newline at end of file diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/README.md b/packages/plugins/trpc/tests/projects/t3-trpc-v11/README.md deleted file mode 100644 index 67943c7fb..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Create T3 App - -This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. - -## What's next? How do I make an app with this? - -We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. - -If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. - -- [Next.js](https://nextjs.org) -- [NextAuth.js](https://next-auth.js.org) -- [Prisma](https://prisma.io) -- [Drizzle](https://orm.drizzle.team) -- [Tailwind CSS](https://tailwindcss.com) -- [tRPC](https://trpc.io) - -## Learn More - -To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: - -- [Documentation](https://create.t3.gg/) -- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials - -You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! - -## How do I deploy this? - -Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/next.config.js b/packages/plugins/trpc/tests/projects/t3-trpc-v11/next.config.js deleted file mode 100644 index 9bfe4a0e2..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful - * for Docker builds. - */ -await import("./src/env.js"); - -/** @type {import("next").NextConfig} */ -const config = {}; - -export default config; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/package-lock.json b/packages/plugins/trpc/tests/projects/t3-trpc-v11/package-lock.json deleted file mode 100644 index b134e32b9..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/package-lock.json +++ /dev/null @@ -1,4803 +0,0 @@ -{ - "name": "trpc11", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "trpc11", - "version": "0.1.0", - "hasInstallScript": true, - "dependencies": { - "@prisma/client": "6.19.x", - "@t3-oss/env-nextjs": "^0.13.0", - "@tanstack/react-query": "^5.50.0", - "@trpc/client": "^11.0.0-rc.446", - "@trpc/next": "^11.0.0-rc.446", - "@trpc/react-query": "^11.0.0-rc.446", - "@trpc/server": "^11.0.0-rc.446", - "geist": "^1.3.0", - "next": "^14.2.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "server-only": "^0.0.1", - "superjson": "^2.2.1", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/eslint": "^8.56.10", - "@types/node": "^20.14.10", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.1.0", - "eslint": "^8.57.0", - "eslint-config-next": "^14.2.4", - "prisma": "6.19.x", - "typescript": "^5.5.3" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@next/env": { - "version": "14.2.5", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "14.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "10.3.10" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.5", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@prisma/client": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.0.tgz", - "integrity": "sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@prisma/config": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.0.tgz", - "integrity": "sha512-Q9TgfnllVehvQziY9lJwRJLGmziX0OimZUEQ/MhCUBoJMSScj2VivCjw/Of2vlO1FfyaHXxrvjZAr7ASl7DVcw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "c12": "3.1.0", - "deepmerge-ts": "7.1.5", - "effect": "3.16.12", - "empathic": "2.0.0" - } - }, - "node_modules/@prisma/debug": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.0.tgz", - "integrity": "sha512-bxzro5vbVqAPkWyDs2A6GpQtRZunD8tyrLmSAchx9u0b+gWCDY6eV+oh5A0YtYT9245dIxQBswckayHuJG4u3w==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.0.tgz", - "integrity": "sha512-RHJGCH/zi017W4CWYWqg0Sv1pquGGFVo8T3auJ9sodDNaiRzbeNldydjaQzszVS8nscdtcvLuJzy7e65C3puqQ==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.16.0", - "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", - "@prisma/fetch-engine": "6.16.0", - "@prisma/get-platform": "6.16.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", - "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.0.tgz", - "integrity": "sha512-Mx5rml0XRIDizhB9eZxSP8c0nMoXYVITTiJJwxlWn9rNCel8mG8NAqIw+vJlN3gPR+kt3IBkP1SQVsplPPpYrA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.16.0", - "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", - "@prisma/get-platform": "6.16.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.0.tgz", - "integrity": "sha512-eaJOOvAoGslSUTjiQrtE9E0hoBdfL43j8SymOGD6LbdrKRNtIoiy6qiBaEr2fNYD+R/Qns7QOwPhl7SVHJayKA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.16.0" - } - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "license": "Apache-2.0" - }, - "node_modules/@swc/helpers": { - "version": "0.5.5", - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@t3-oss/env-core": { - "version": "0.13.8", - "license": "MIT", - "peerDependencies": { - "arktype": "^2.1.0", - "typescript": ">=5.0.0", - "valibot": "^1.0.0-beta.7 || ^1.0.0", - "zod": "^3.24.0 || ^4.0.0-beta.0" - }, - "peerDependenciesMeta": { - "arktype": { - "optional": true - }, - "typescript": { - "optional": true - }, - "valibot": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@t3-oss/env-nextjs": { - "version": "0.13.8", - "license": "MIT", - "dependencies": { - "@t3-oss/env-core": "0.13.8" - }, - "peerDependencies": { - "arktype": "^2.1.0", - "typescript": ">=5.0.0", - "valibot": "^1.0.0-beta.7 || ^1.0.0", - "zod": "^3.24.0 || ^4.0.0-beta.0" - }, - "peerDependenciesMeta": { - "arktype": { - "optional": true - }, - "typescript": { - "optional": true - }, - "valibot": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.52.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.52.0", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.52.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18.0.0" - } - }, - "node_modules/@trpc/client": { - "version": "11.0.0-rc.485+1c1d824cd", - "funding": [ - "https://trpc.io/sponsor" - ], - "license": "MIT", - "peerDependencies": { - "@trpc/server": "11.0.0-rc.485+1c1d824cd" - } - }, - "node_modules/@trpc/next": { - "version": "11.0.0-rc.485+1c1d824cd", - "funding": [ - "https://trpc.io/sponsor" - ], - "license": "MIT", - "peerDependencies": { - "@tanstack/react-query": "^5.49.2", - "@trpc/client": "11.0.0-rc.485+1c1d824cd", - "@trpc/react-query": "11.0.0-rc.485+1c1d824cd", - "@trpc/server": "11.0.0-rc.485+1c1d824cd", - "next": "*", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@tanstack/react-query": { - "optional": true - }, - "@trpc/react-query": { - "optional": true - } - } - }, - "node_modules/@trpc/react-query": { - "version": "11.0.0-rc.485+1c1d824cd", - "funding": [ - "https://trpc.io/sponsor" - ], - "license": "MIT", - "peerDependencies": { - "@tanstack/react-query": "^5.49.2", - "@trpc/client": "11.0.0-rc.485+1c1d824cd", - "@trpc/server": "11.0.0-rc.485+1c1d824cd", - "react": ">=18.2.0", - "react-dom": ">=18.2.0" - } - }, - "node_modules/@trpc/server": { - "version": "11.0.0-rc.485+1c1d824cd", - "funding": [ - "https://trpc.io/sponsor" - ], - "license": "MIT" - }, - "node_modules/@types/eslint": { - "version": "8.56.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/type-utils": "8.2.0", - "@typescript-eslint/utils": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/utils": "8.2.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.2.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "dev": true, - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.12.1", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.1.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.10.0", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "3.1.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/c12": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.3", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^16.6.1", - "exsolve": "^1.0.7", - "giget": "^2.0.0", - "jiti": "^2.4.2", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.2.0", - "rc9": "^2.1.2" - }, - "peerDependencies": { - "magicast": "^0.3.5" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/copy-anything": { - "version": "3.0.5", - "license": "MIT", - "dependencies": { - "is-what": "^4.1.8" - }, - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.6", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/effect": { - "version": "3.16.12", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", - "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "fast-check": "^3.23.1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "license": "MIT" - }, - "node_modules/empathic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-abstract": { - "version": "1.23.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.0.19", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "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", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "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.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-next": { - "version": "14.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "14.2.5", - "@rushstack/eslint-patch": "^1.3.3", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" - }, - "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/typescript-estree": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/minimatch": { - "version": "9.0.3", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "dev": true, - "license": "ISC", - "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "~5.1.3", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.9.1", - "axobject-query": "~3.1.1", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.19", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.0" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.35.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.8", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.17.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/geist": { - "version": "1.3.1", - "license": "SIL OPEN FONT LICENSE", - "peerDependencies": { - "next": ">=13.2.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.7.6", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/giget": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, - "node_modules/glob": { - "version": "10.3.10", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-what": { - "version": "4.1.16", - "license": "MIT", - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - } - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", - "devOptional": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "dev": true, - "license": "ISC" - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.7", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/next": { - "version": "14.2.5", - "license": "MIT", - "dependencies": { - "@next/env": "14.2.5", - "@swc/helpers": "0.5.5", - "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", - "postcss": "8.4.31", - "styled-jsx": "5.1.1" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=18.17.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.5", - "@next/swc-darwin-x64": "14.2.5", - "@next/swc-linux-arm64-gnu": "14.2.5", - "@next/swc-linux-arm64-musl": "14.2.5", - "@next/swc-linux-x64-gnu": "14.2.5", - "@next/swc-linux-x64-musl": "14.2.5", - "@next/swc-win32-arm64-msvc": "14.2.5", - "@next/swc-win32-ia32-msvc": "14.2.5", - "@next/swc-win32-x64-msvc": "14.2.5" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/nypm": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", - "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", - "pathe": "^2.0.3", - "pkg-types": "^2.2.0", - "tinyexec": "^1.0.1" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.0.1", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prisma": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.0.tgz", - "integrity": "sha512-TTh+H1Kw8N68KN9cDzdAyMroqMOvdCO/Z+kS2wKEVYR1nuR21qH5Q/Db/bZHsAgw7l/TPHtM/veG5VABcdwPDw==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/config": "6.16.0", - "@prisma/engines": "6.16.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" - } - }, - "node_modules/react": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "dev": true, - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.6.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/server-only": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/string.prototype.includes": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", - "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.1", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/superjson": { - "version": "2.2.1", - "license": "MIT", - "dependencies": { - "copy-anything": "^3.0.2" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.6.3", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.5.4", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json b/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json deleted file mode 100644 index 12a77902d..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "trpc11", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "build": "next build", - "db:generate": "prisma migrate dev", - "db:migrate": "prisma migrate deploy", - "db:push": "prisma db push", - "db:studio": "prisma studio", - "dev": "next dev", - "postinstall": "prisma generate", - "lint": "next lint", - "start": "next start" - }, - "dependencies": { - "@prisma/client": "6.19.x", - "@t3-oss/env-nextjs": "^0.13.0", - "@tanstack/react-query": "^5.50.0", - "@trpc/client": "^11.0.0-rc.446", - "@trpc/next": "^11.0.0-rc.446", - "@trpc/react-query": "^11.0.0-rc.446", - "@trpc/server": "^11.0.0-rc.446", - "geist": "^1.3.0", - "next": "^14.2.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "server-only": "^0.0.1", - "superjson": "^2.2.1", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/eslint": "^8.56.10", - "@types/node": "^20.14.10", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.1.0", - "eslint": "^8.57.0", - "eslint-config-next": "^14.2.4", - "prisma": "6.19.x", - "typescript": "^5.5.3" - }, - "ct3aMetadata": { - "initVersion": "7.37.0" - }, - "packageManager": "npm@10.2.3" -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/prisma/schema.prisma b/packages/plugins/trpc/tests/projects/t3-trpc-v11/prisma/schema.prisma deleted file mode 100644 index a28fea9fb..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/prisma/schema.prisma +++ /dev/null @@ -1,31 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -////////////////////////////////////////////////////////////////////////////////////////////// - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -generator client { - provider = "prisma-client-js" -} - -model User { - id Int @id() @default(autoincrement()) - email String @unique() - posts Post[] -} - -model Post { - id Int @id() @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int - - @@index([name]) -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/public/favicon.ico b/packages/plugins/trpc/tests/projects/t3-trpc-v11/public/favicon.ico deleted file mode 100644 index 60c702aac13409c82c4040f8fee9603eb84aa10c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 zcmeHOX^b4j6>hH&u3!wtgoMM(Da<7x5Rn8Tjvq)?zybua1c*c20{$U^Aj%>HD9R-z zBuWB;ARB>eYK+y}Dk#s*$8Q(p+iLA_;N7bn84x`l%#I{rywlCmbkAPayBK)27Rhm!$WXNYV+Q zK^4@P%189Q__>DsD^FL?7r_P=JJvIlKl+CJ62dyd9WqHP5Sp7xG3? zPX~|xApI@EB+@r<8Zj2@M^QA-H<*IF*CS2am*|i;*E8hDd|es0ZRN*eT}q4f={qph zUyoWUdZ+R8ip52QA+%;l|Sa2^7!PrYAH1GAw4nmfKx zy1+M;Td^MB<(cfVs$m?`u57IHH)D=|NWm@3zi7tCFgENLSn7k=Pdv~@u`r2!VJ-Hn z5cXiS66w9>LT56iNCfU;)=ma= z`c9MYdEO$lXDjiAZma0~qmy`0Ud=wxm3KG>+B=)kiuq~siOx6F`)WK*<@aK}q_nFk z=W_Xo|D8k=&&!fe^n@YJ_ToIDgFft$u(^~7d^hv_v^bCawEFQf^jDeWqrcR6S<-hm zzt3;Z#bYA(s$u7s3+5#DVP-&b)99=$<^0;j3 zcc)NTm?l#!%OgJ;80Z7t%UlN9>UibS>6}kssAr=rpgqWS-2-@jo;Z(u;uA5p57xgo zI0neFT|+sU%cxe{+k^AUuVIL^eX;Kh)-j;ZL#;@rXxqP5TdFpnH#&uyA7>w*DZVL^MIqKm_{4qbT z46X9@-3V1(K8eflW%)qJ|3tvB*;bOy=By;paJ=otl~IG8!ZC!Zx){7a=ln4@zlz(V z7_;4!AJJMDRWX`AY2VT|#s$X@PdzK-T;Cwj zne8F?PcC3MKk<6qh;i;b3A`O4yiT@P9^OMkLknOd-T*uDYt$b@NVA1^;H+n%Evu7k z>pb$3Xk0dOYE15jHpV~_t(ZqPKm3n>Lf!4L`e|*bmEqhbhbHxN@*R{YCoA0!{t9D< z0rS(%aWflbbWbUxZ)$$W8ML~>yt1+aZJ3*dF|E8+{1N%xP5HMN-`hk?7#G{oA8!yQ zItP22wv^7IzFj^8bPprM{pCAx9^42%$E4xQDr*&g=#+mBoDDzCl#kAa&+K;Sa(**; z)E3lx6Km5h?MAzv?PMIaf}i>te(+aCy+em3%;8I#;TH2vtP6w}Vahi-HQy%#tysSg z73uS&TftwgXv=7vaQstao88lj{;Ha`Y{og-R9*p(zC3v2G_ByLx>&>Sg}!UPZM37{ z>Basy&#Z6gV1VnO7GpiK)nUBcX#LkJemal)mUNRv0cJMfcj3f=Dz^j|@H+FCaH(7$6r#>64S<{vsG@JOVy1oSK4xI(+V+VV|j!MdVwyb&3DSn!Z zW3G1OR$D#3*?L50mMZRep!apV>Ydsl|2+$1T6rh6M@FW$H2ME+kz+>T7Wl^_o9vCD6fxs zw5dex9l)Jn7RI#lcJW8_p3YR>!}x930X2N`de0kKO7H%-T=dC&PcQue_TD(`)d{Ti zpVECPFYhF77eC3Qa|(3&+O+N)x;5nYT$7zD;-a%M-T>Z>_WovzZD*cO#ky(fPVm!M z4>50XE?F;*jp_Cb#^0xcejdVtG(4@Ab%LME8grb(VULnMQ(qTr?XlS4sA=Z%WpG}t z#@)bAGHE<}Ce}x+=VD)Aj=U1KY1`*%OSkaTSaNZniT#y)LG`(S^d#o(f0N$S=E0Xm z69nua-)1-RfN`**lRLwjZKf-mfScCTMmtQlmSkn&*%Qh=t8_ZBe~{3IHMCdn2^iBb zU@Z0dnsO%gtl?N6Y{&Y!ir&Ac*2mk5ac|mxL_VZh2;?)RIUwSqJpgNKaYjEF@;@WI zV;5<~G|ty}hr~8c`6=kme>Q^rmKXb<0b#1W2{PGdGuxm%Zdt`cMch0MyXbn%Lwe)X zm_Ofrn*7V_#@MFAI1Y+wEV-6^4ls%5b>Nb>VQu|ugs~#hQ+hYypVk$7do)3>4&JN5 z*Qv&IioJq8V&L9GYy;NYm7v3!Od*?f)&p$Ie z=7xjyDDv&@jzB)Sq
c--SY9FM2yJ2IM#O`-=V2OZPO;(?CxHJq`3Uu%~L^f2g^2 A#Q*>R diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/schema.zmodel b/packages/plugins/trpc/tests/projects/t3-trpc-v11/schema.zmodel deleted file mode 100644 index fd6a8118a..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/schema.zmodel +++ /dev/null @@ -1,41 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -plugin trpc { - provider = "../../../dist" - output = "src/server/api/routers/generated" - version = "v11" - importCreateRouter = "../../generated-router-helper" - importProcedure = "../../generated-router-helper" - generateClientHelpers = "react" -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] - - @@allow('all', true) -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int - - @@index([name]) - @@allow('all', true) -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/_components/post.tsx b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/_components/post.tsx deleted file mode 100644 index 8455c4a53..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/_components/post.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -import { api } from '~/trpc/react'; -import styles from '../index.module.css'; - -export function LatestPost() { - const { data: latestPost } = api.post.findFirst.useQuery( - { - orderBy: { createdAt: 'desc' }, - include: { author: true }, - }, - { staleTime: 1000 * 60 } - ); - - const [latestPost1] = api.post.findFirst.useSuspenseQuery( - { - orderBy: { createdAt: 'desc' }, - include: { author: true }, - }, - { staleTime: 1000 * 60 } - ); - console.log(latestPost1?.author.email); - - api.post.findMany.useInfiniteQuery( - { - take: 10, - orderBy: { createdAt: 'desc' }, - include: { author: true }, - }, - { - getNextPageParam: (lastPage) => ({ - id: lastPage?.[lastPage.length - 1]?.id, - }), - } - ); - - const utils = api.useUtils(); - const [name, setName] = useState(''); - const createPost = api.post.create.useMutation({ - onSuccess: async () => { - await utils.post.invalidate(); - setName(''); - }, - }); - - return ( -
- {latestPost ? ( -

- Your most recent post: {latestPost.name} by {latestPost.author.email} -

- ) : ( -

You have no posts yet.

- )} - -
{ - e.preventDefault(); - createPost.mutate({ data: { name, author: { connect: { id: 1 } } } }); - }} - className={styles.form} - > - setName(e.target.value)} - className={styles.input} - /> - -
-
- ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/api/trpc/[trpc]/route.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/api/trpc/[trpc]/route.ts deleted file mode 100644 index 5fbd827d9..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/api/trpc/[trpc]/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; -import { type NextRequest } from "next/server"; - -import { env } from "~/env"; -import { appRouter } from "~/server/api/root"; -import { createTRPCContext } from "~/server/api/trpc"; - -/** - * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when - * handling a HTTP request (e.g. when you make requests from Client Components). - */ -const createContext = async (req: NextRequest) => { - return createTRPCContext({ - headers: req.headers, - }); -}; - -const handler = (req: NextRequest) => - fetchRequestHandler({ - endpoint: "/api/trpc", - req, - router: appRouter, - createContext: () => createContext(req), - onError: - env.NODE_ENV === "development" - ? ({ path, error }) => { - console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}` - ); - } - : undefined, - }); - -export { handler as GET, handler as POST }; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/index.module.css b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/index.module.css deleted file mode 100644 index fac9982a3..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/index.module.css +++ /dev/null @@ -1,177 +0,0 @@ -.main { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 100vh; - background-image: linear-gradient(to bottom, #2e026d, #15162c); -} - -.container { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 3rem; - padding: 4rem 1rem; -} - -@media (min-width: 640px) { - .container { - max-width: 640px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 768px; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 1024px; - } -} - -@media (min-width: 1280px) { - .container { - max-width: 1280px; - } -} - -@media (min-width: 1536px) { - .container { - max-width: 1536px; - } -} - -.title { - font-size: 3rem; - line-height: 1; - font-weight: 800; - letter-spacing: -0.025em; - margin: 0; - color: white; -} - -@media (min-width: 640px) { - .title { - font-size: 5rem; - } -} - -.pinkSpan { - color: hsl(280 100% 70%); -} - -.cardRow { - display: grid; - grid-template-columns: repeat(1, minmax(0, 1fr)); - gap: 1rem; -} - -@media (min-width: 640px) { - .cardRow { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (min-width: 768px) { - .cardRow { - gap: 2rem; - } -} - -.card { - max-width: 20rem; - display: flex; - flex-direction: column; - gap: 1rem; - padding: 1rem; - border-radius: 0.75rem; - color: white; - background-color: rgb(255 255 255 / 0.1); -} - -.card:hover { - background-color: rgb(255 255 255 / 0.2); - transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); -} - -.cardTitle { - font-size: 1.5rem; - line-height: 2rem; - font-weight: 700; - margin: 0; -} - -.cardText { - font-size: 1.125rem; - line-height: 1.75rem; -} - -.showcaseContainer { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; -} - -.showcaseText { - color: white; - text-align: center; - font-size: 1.5rem; - line-height: 2rem; -} - -.authContainer { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; -} - -.loginButton { - border-radius: 9999px; - background-color: rgb(255 255 255 / 0.1); - padding: 0.75rem 2.5rem; - font-weight: 600; - color: white; - text-decoration-line: none; - transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); -} - -.loginButton:hover { - background-color: rgb(255 255 255 / 0.2); -} - -.form { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.input { - width: 100%; - border-radius: 9999px; - padding: 0.5rem 1rem; - color: black; -} - -.submitButton { - all: unset; - border-radius: 9999px; - background-color: rgb(255 255 255 / 0.1); - padding: 0.75rem 2.5rem; - font-weight: 600; - color: white; - text-align: center; - transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); -} - -.submitButton:hover { - background-color: rgb(255 255 255 / 0.2); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/layout.tsx b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/layout.tsx deleted file mode 100644 index 56d99f3f6..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/layout.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import "~/styles/globals.css"; - -import { GeistSans } from "geist/font/sans"; -import { type Metadata } from "next"; - -import { TRPCReactProvider } from "~/trpc/react"; - -export const metadata: Metadata = { - title: "Create T3 App", - description: "Generated by create-t3-app", - icons: [{ rel: "icon", url: "/favicon.ico" }], -}; - -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - {children} - - - ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/page.tsx b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/page.tsx deleted file mode 100644 index 79efd1d9c..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/app/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Link from 'next/link'; - -import { LatestPost } from '~/app/_components/post'; -import { HydrateClient } from '~/trpc/server'; -import styles from './index.module.css'; - -export default async function Home() { - return ( - -
-
-

- Create T3 App -

-
- -

First Steps →

-
- Just the basics - Everything you need to know to set up your database and - authentication. -
- - -

Documentation →

-
- Learn more about Create T3 App, the libraries it uses, and how to deploy it. -
- -
- -
-
-
- ); -} - -export const dynamic = 'force-dynamic'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/env.js b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/env.js deleted file mode 100644 index 829273519..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/env.js +++ /dev/null @@ -1,42 +0,0 @@ -import { createEnv } from '@t3-oss/env-nextjs'; -import { z } from 'zod'; - -export const env = createEnv({ - /** - * Specify your server-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. - */ - server: { - NODE_ENV: z - .enum(['development', 'test', 'production']) - .default('development'), - }, - - /** - * Specify your client-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. To expose them to the client, prefix them with - * `NEXT_PUBLIC_`. - */ - client: { - // NEXT_PUBLIC_CLIENTVAR: z.string(), - }, - - /** - * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. - * middlewares) or client-side so we need to destruct manually. - */ - runtimeEnv: { - NODE_ENV: process.env.NODE_ENV, - // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, - }, - /** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially - * useful for Docker builds. - */ - skipValidation: !!process.env.SKIP_ENV_VALIDATION, - /** - * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and - * `SOME_VAR=''` will throw an error. - */ - emptyStringAsUndefined: true, -}); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/root.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/root.ts deleted file mode 100644 index 63d72797d..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/root.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createCallerFactory } from '~/server/api/trpc'; -import { createRouter } from './routers/generated/routers'; - -/** - * This is the primary router for your server. - * - * All routers added in /api/routers should be manually added here. - */ -export const appRouter = createRouter(); - -// export type definition of API -export type AppRouter = typeof appRouter; - -/** - * Create a server-side caller for the tRPC API. - * @example - * const trpc = createCaller(createContext); - * const res = await trpc.post.all(); - * ^? Post[] - */ -export const createCaller = createCallerFactory(appRouter); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated-router-helper.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated-router-helper.ts deleted file mode 100644 index 90eccfe13..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated-router-helper.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { createTRPCRouter } from '../trpc'; -export { publicProcedure as procedure } from '../trpc'; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/Post.react.type.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/Post.react.type.ts deleted file mode 100644 index 989f089bd..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/Post.react.type.ts +++ /dev/null @@ -1,584 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { Prisma } from '@prisma/client'; -import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; -import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; -import type { UseTRPCSuspenseQueryOptions, UseTRPCSuspenseQueryResult, UseTRPCSuspenseInfiniteQueryOptions, UseTRPCSuspenseInfiniteQueryResult } from '@trpc/react-query/shared'; - -export interface ClientType { - aggregate: { - - useQuery: >( - input: Prisma.Subset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.GetPostAggregateType, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >( - input: Prisma.Subset, - opts?: UseTRPCSuspenseQueryOptions, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions, Error> - ) => UseTRPCSuspenseInfiniteQueryResult, TRPCClientErrorLike, T>; - - }; - createMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostCreateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables?: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - create: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostCreateArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> - }; - - }; - deleteMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostDeleteManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables?: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - delete: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostDeleteArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> - }; - - }; - findFirst: { - - useQuery: | null>( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions | null, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions | null, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.PostGetPayload | null, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: | null>( - input?: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions | null, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions | null, Error> - ) => UseTRPCSuspenseInfiniteQueryResult | null, TRPCClientErrorLike, T>; - - }; - findFirstOrThrow: { - - useQuery: >( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.PostGetPayload, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >( - input?: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions, Error> - ) => UseTRPCSuspenseInfiniteQueryResult, TRPCClientErrorLike, T>; - - }; - findMany: { - - useQuery: >>( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions>, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions>, Error> - ) => UseTRPCInfiniteQueryResult< - Array>, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >>( - input?: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions>, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions>, Error> - ) => UseTRPCSuspenseInfiniteQueryResult>, TRPCClientErrorLike, T>; - - }; - findUnique: { - - useQuery: | null>( - input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions | null, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions | null, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.PostGetPayload | null, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: | null>( - input: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions | null, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions | null, Error> - ) => UseTRPCSuspenseInfiniteQueryResult | null, TRPCClientErrorLike, T>; - - }; - findUniqueOrThrow: { - - useQuery: >( - input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.PostGetPayload, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >( - input: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions, Error> - ) => UseTRPCSuspenseInfiniteQueryResult, TRPCClientErrorLike, T>; - - }; - groupBy: { - - useQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , TData = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors>( - input: Prisma.SubsetIntersection & InputErrors, - opts?: UseTRPCQueryOptions<{} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >( - input: Omit & InputErrors, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions : InputErrors, Error> - ) => UseTRPCInfiniteQueryResult< - {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , TData = {} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors>( - input: Prisma.SubsetIntersection & InputErrors, - opts?: UseTRPCSuspenseQueryOptions<{} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.PostGroupByArgs['orderBy'] } - : { orderBy?: Prisma.PostGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >( - input: Omit & InputErrors, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions : InputErrors, Error> - ) => UseTRPCSuspenseInfiniteQueryResult<{} extends InputErrors ? Prisma.GetPostGroupByPayload : InputErrors, TRPCClientErrorLike, T>; - - }; - updateMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostUpdateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - update: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostUpdateArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> - }; - - }; - upsert: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.PostUpsertArgs, - TRPCClientErrorLike, - Prisma.PostGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.PostGetPayload, Context>) => Promise> - }; - - }; - count: { - - useQuery: - : number>( - input?: Prisma.Subset, - opts?: UseTRPCQueryOptions<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions - : number, Error> - ) => UseTRPCInfiniteQueryResult< - 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: - : number>( - input?: Prisma.Subset, - opts?: UseTRPCSuspenseQueryOptions<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions - : number, Error> - ) => UseTRPCSuspenseInfiniteQueryResult<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, TRPCClientErrorLike, T>; - - }; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/User.react.type.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/User.react.type.ts deleted file mode 100644 index e8e0316e0..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/User.react.type.ts +++ /dev/null @@ -1,584 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { Prisma } from '@prisma/client'; -import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client'; -import type { UseTRPCMutationOptions, UseTRPCMutationResult, UseTRPCQueryOptions, UseTRPCQueryResult, UseTRPCInfiniteQueryOptions, UseTRPCInfiniteQueryResult } from '@trpc/react-query/shared'; -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; -import type { UseTRPCSuspenseQueryOptions, UseTRPCSuspenseQueryResult, UseTRPCSuspenseInfiniteQueryOptions, UseTRPCSuspenseInfiniteQueryResult } from '@trpc/react-query/shared'; - -export interface ClientType { - aggregate: { - - useQuery: >( - input: Prisma.Subset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.GetUserAggregateType, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >( - input: Prisma.Subset, - opts?: UseTRPCSuspenseQueryOptions, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions, Error> - ) => UseTRPCSuspenseInfiniteQueryResult, TRPCClientErrorLike, T>; - - }; - createMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserCreateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables?: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - create: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserCreateArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> - }; - - }; - deleteMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserDeleteManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables?: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - delete: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserDeleteArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> - }; - - }; - findFirst: { - - useQuery: | null>( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions | null, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions | null, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.UserGetPayload | null, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: | null>( - input?: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions | null, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions | null, Error> - ) => UseTRPCSuspenseInfiniteQueryResult | null, TRPCClientErrorLike, T>; - - }; - findFirstOrThrow: { - - useQuery: >( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.UserGetPayload, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >( - input?: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions, Error> - ) => UseTRPCSuspenseInfiniteQueryResult, TRPCClientErrorLike, T>; - - }; - findMany: { - - useQuery: >>( - input?: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions>, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions>, Error> - ) => UseTRPCInfiniteQueryResult< - Array>, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >>( - input?: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions>, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions>, Error> - ) => UseTRPCSuspenseInfiniteQueryResult>, TRPCClientErrorLike, T>; - - }; - findUnique: { - - useQuery: | null>( - input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions | null, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions | null, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.UserGetPayload | null, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: | null>( - input: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions | null, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions | null, Error> - ) => UseTRPCSuspenseInfiniteQueryResult | null, TRPCClientErrorLike, T>; - - }; - findUniqueOrThrow: { - - useQuery: >( - input: Prisma.SelectSubset, - opts?: UseTRPCQueryOptions, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions, Error> - ) => UseTRPCInfiniteQueryResult< - Prisma.UserGetPayload, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >( - input: Prisma.SelectSubset, - opts?: UseTRPCSuspenseQueryOptions, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions, Error> - ) => UseTRPCSuspenseInfiniteQueryResult, TRPCClientErrorLike, T>; - - }; - groupBy: { - - useQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , TData = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors>( - input: Prisma.SubsetIntersection & InputErrors, - opts?: UseTRPCQueryOptions<{} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >( - input: Omit & InputErrors, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions : InputErrors, Error> - ) => UseTRPCInfiniteQueryResult< - {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - , TData = {} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors>( - input: Prisma.SubsetIntersection & InputErrors, - opts?: UseTRPCSuspenseQueryOptions<{} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: >, - Prisma.Extends<'take', Prisma.Keys> - >, - OrderByArg extends Prisma.True extends HasSelectOrTake - ? { orderBy: Prisma.UserGroupByArgs['orderBy'] } - : { orderBy?: Prisma.UserGroupByArgs['orderBy'] }, - OrderFields extends Prisma.ExcludeUnderscoreKeys>>, - ByFields extends Prisma.MaybeTupleToUnion, - ByValid extends Prisma.Has, - HavingFields extends Prisma.GetHavingFields, - HavingValid extends Prisma.Has, - ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, - InputErrors extends ByEmpty extends Prisma.True - ? `Error: "by" must not be empty.` - : HavingValid extends Prisma.False - ? { - [P in HavingFields]: P extends ByFields - ? never - : P extends string - ? `Error: Field "${P}" used in "having" needs to be provided in "by".` - : [ - Error, - 'Field ', - P, - ` in "having" needs to be provided in "by"`, - ] - }[HavingFields] - : 'take' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "take", you also need to provide "orderBy"' - : 'skip' extends Prisma.Keys - ? 'orderBy' extends Prisma.Keys - ? ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - : 'Error: If you provide "skip", you also need to provide "orderBy"' - : ByValid extends Prisma.True - ? {} - : { - [P in OrderFields]: P extends ByFields - ? never - : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` - }[OrderFields] - >( - input: Omit & InputErrors, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions : InputErrors, Error> - ) => UseTRPCSuspenseInfiniteQueryResult<{} extends InputErrors ? Prisma.GetUserGroupByPayload : InputErrors, TRPCClientErrorLike, T>; - - }; - updateMany: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserUpdateManyArgs, - TRPCClientErrorLike, - Prisma.BatchPayload, - Context - >) => - Omit, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.BatchPayload, Context>) => Promise - }; - - }; - update: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserUpdateArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> - }; - - }; - upsert: { - - useMutation: (opts?: UseTRPCMutationOptions< - Prisma.UserUpsertArgs, - TRPCClientErrorLike, - Prisma.UserGetPayload, - Context - >) => - Omit, TRPCClientErrorLike, Prisma.SelectSubset, Context>, 'mutateAsync'> & { - mutateAsync: - (variables: T, opts?: UseTRPCMutationOptions, Prisma.UserGetPayload, Context>) => Promise> - }; - - }; - count: { - - useQuery: - : number>( - input?: Prisma.Subset, - opts?: UseTRPCQueryOptions<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, TData, Error> - ) => UseTRPCQueryResult< - TData, - TRPCClientErrorLike - >; - useInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCInfiniteQueryOptions - : number, Error> - ) => UseTRPCInfiniteQueryResult< - 'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, - TRPCClientErrorLike, - T - >; - useSuspenseQuery: - : number>( - input?: Prisma.Subset, - opts?: UseTRPCSuspenseQueryOptions<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, TData, Error> - ) => UseTRPCSuspenseQueryResult>; - useSuspenseInfiniteQuery: ( - input?: Omit, 'cursor'>, - opts?: UseTRPCSuspenseInfiniteQueryOptions - : number, Error> - ) => UseTRPCSuspenseInfiniteQueryResult<'select' extends keyof T - ? T['select'] extends true - ? number - : Prisma.GetScalarType - : number, TRPCClientErrorLike, T>; - - }; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/react.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/react.ts deleted file mode 100644 index 604c4c1c9..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/react.ts +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { AnyTRPCRouter as AnyRouter } from '@trpc/server'; -import type { CreateTRPCReactOptions } from '@trpc/react-query/shared'; -import { type CreateTRPCReact, createTRPCReact as _createTRPCReact } from '@trpc/react-query'; -import type { DeepOverrideAtPath } from './utils'; -import { ClientType as UserClientType } from "./User.react.type"; -import { ClientType as PostClientType } from "./Post.react.type"; - -export function createTRPCReact< - TRouter extends AnyRouter, - TPath extends string | undefined = undefined, - TSSRContext = unknown ->(opts?: CreateTRPCReactOptions) { - const r: CreateTRPCReact = _createTRPCReact(opts); - return r as DeepOverrideAtPath, ClientType, TPath>; -} - -export interface ClientType { - user: UserClientType; - post: PostClientType; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/utils.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/utils.ts deleted file mode 100644 index 830996b8c..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/client/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -type Primitive = string | Function | number | boolean | Symbol | undefined | null; - -/** - * Recursively merges `T` and `R`. If there's a shared key, use `R`'s field type to overwrite `T`. - */ -export type DeepOverride = T extends Primitive - ? R - : R extends Primitive - ? R - : { - [K in keyof T]: K extends keyof R ? DeepOverride : T[K]; - } & { - [K in Exclude]: R[K]; - }; - -/** - * Traverse to `Path` (denoted by dot separated string literal type) in `T`, and starting from there, - * recursively merge with `R`. - */ -export type DeepOverrideAtPath = Path extends undefined - ? DeepOverride - : Path extends `${infer P1}.${infer P2}` - ? P1 extends keyof T - ? Omit & Record>> - : never - : Path extends keyof T - ? Omit & Record> - : never; - -// Utility type from 'trpc-nuxt' -export type KeysOf = Array; - -// Utility type from 'trpc-nuxt' -export type PickFrom> = T extends Array - ? T - : T extends Record - ? keyof T extends K[number] - ? T - : K[number] extends never - ? T - : Pick - : T; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/helper.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/helper.ts deleted file mode 100644 index 183476950..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/helper.ts +++ /dev/null @@ -1,74 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { TRPCError } from '@trpc/server'; -import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; - -export async function checkMutate(promise: Promise): Promise { - try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - if (err.meta?.reason === 'RESULT_NOT_READABLE') { - // unable to readback data - return undefined; - } else { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }); - } - } else { - throw err; - } - } - -} - -export async function checkRead(promise: Promise): Promise { - try { - return await promise; - } catch (err: any) { - if (isPrismaClientKnownRequestError(err)) { - if (err.code === 'P2004') { - // rejected by policy - throw new TRPCError({ - code: 'FORBIDDEN', - message: err.message, - cause: err, - }); - } else if (err.code === 'P2025') { - // not found - throw new TRPCError({ - code: 'NOT_FOUND', - message: err.message, - cause: err, - }); - } else { - // request error - throw new TRPCError({ - code: 'BAD_REQUEST', - message: err.message, - cause: err, - }) - } - } else { - throw err; - } - } - -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/Post.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/Post.router.ts deleted file mode 100644 index ae11381f9..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/Post.router.ts +++ /dev/null @@ -1,49 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { db } from "."; -import { createTRPCRouter } from "../../generated-router-helper"; -import { procedure } from "../../generated-router-helper"; -import * as _Schema from '@zenstackhq/runtime/zod/input'; -const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; -import { checkRead, checkMutate } from '../helper'; - -export default function createRouter() { - return createTRPCRouter({ - - aggregate: procedure.input($Schema.PostInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).post.aggregate(input as any))), - - createMany: procedure.input($Schema.PostInputSchema.createMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.createMany(input as any))), - - create: procedure.input($Schema.PostInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.create(input as any))), - - deleteMany: procedure.input($Schema.PostInputSchema.deleteMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.deleteMany(input as any))), - - delete: procedure.input($Schema.PostInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.delete(input as any))), - - findFirst: procedure.input($Schema.PostInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findFirst(input as any))), - - findFirstOrThrow: procedure.input($Schema.PostInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findFirstOrThrow(input as any))), - - findMany: procedure.input($Schema.PostInputSchema.findMany.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.findMany(input as any))), - - findUnique: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUnique(input as any))), - - findUniqueOrThrow: procedure.input($Schema.PostInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).post.findUniqueOrThrow(input as any))), - - groupBy: procedure.input($Schema.PostInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).post.groupBy(input as any))), - - updateMany: procedure.input($Schema.PostInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.updateMany(input as any))), - - update: procedure.input($Schema.PostInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.update(input as any))), - - upsert: procedure.input($Schema.PostInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).post.upsert(input as any))), - - count: procedure.input($Schema.PostInputSchema.count.optional()).query(({ ctx, input }) => checkRead(db(ctx).post.count(input as any))), - - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/User.router.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/User.router.ts deleted file mode 100644 index 7e81f0052..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/User.router.ts +++ /dev/null @@ -1,49 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import { db } from "."; -import { createTRPCRouter } from "../../generated-router-helper"; -import { procedure } from "../../generated-router-helper"; -import * as _Schema from '@zenstackhq/runtime/zod/input'; -const $Schema: typeof _Schema = (_Schema as any).default ?? _Schema; -import { checkRead, checkMutate } from '../helper'; - -export default function createRouter() { - return createTRPCRouter({ - - aggregate: procedure.input($Schema.UserInputSchema.aggregate).query(({ ctx, input }) => checkRead(db(ctx).user.aggregate(input as any))), - - createMany: procedure.input($Schema.UserInputSchema.createMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.createMany(input as any))), - - create: procedure.input($Schema.UserInputSchema.create).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.create(input as any))), - - deleteMany: procedure.input($Schema.UserInputSchema.deleteMany.optional()).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.deleteMany(input as any))), - - delete: procedure.input($Schema.UserInputSchema.delete).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.delete(input as any))), - - findFirst: procedure.input($Schema.UserInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findFirst(input as any))), - - findFirstOrThrow: procedure.input($Schema.UserInputSchema.findFirst.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findFirstOrThrow(input as any))), - - findMany: procedure.input($Schema.UserInputSchema.findMany.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.findMany(input as any))), - - findUnique: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUnique(input as any))), - - findUniqueOrThrow: procedure.input($Schema.UserInputSchema.findUnique).query(({ ctx, input }) => checkRead(db(ctx).user.findUniqueOrThrow(input as any))), - - groupBy: procedure.input($Schema.UserInputSchema.groupBy).query(({ ctx, input }) => checkRead(db(ctx).user.groupBy(input as any))), - - updateMany: procedure.input($Schema.UserInputSchema.updateMany).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.updateMany(input as any))), - - update: procedure.input($Schema.UserInputSchema.update).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.update(input as any))), - - upsert: procedure.input($Schema.UserInputSchema.upsert).mutation(async ({ ctx, input }) => checkMutate(db(ctx).user.upsert(input as any))), - - count: procedure.input($Schema.UserInputSchema.count.optional()).query(({ ctx, input }) => checkRead(db(ctx).user.count(input as any))), - - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/index.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/index.ts deleted file mode 100644 index a71da919e..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/routers/generated/routers/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - -import type { AnyTRPCRouter as AnyRouter } from "@trpc/server"; -import type { PrismaClient } from "@prisma/client"; -import { createTRPCRouter } from "../../generated-router-helper"; -import createUserRouter from "./User.router"; -import createPostRouter from "./Post.router"; - -export function db(ctx: any) { - if (!ctx.prisma) { - throw new Error('Missing "prisma" field in trpc context'); - } - return ctx.prisma as PrismaClient; -} - -export function createRouter() { - return createTRPCRouter({ - user: createUserRouter(), - post: createPostRouter(), - } - ); -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/trpc.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/trpc.ts deleted file mode 100644 index a685a033b..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/api/trpc.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC } from '@trpc/server'; -import superjson from 'superjson'; -import { ZodError } from 'zod'; - -import { db } from '~/server/db'; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - return { - db, - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an artificial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/db.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/db.ts deleted file mode 100644 index 07dc0271a..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/server/db.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -import { env } from "~/env"; - -const createPrismaClient = () => - new PrismaClient({ - log: - env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], - }); - -const globalForPrisma = globalThis as unknown as { - prisma: ReturnType | undefined; -}; - -export const db = globalForPrisma.prisma ?? createPrismaClient(); - -if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/styles/globals.css b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/styles/globals.css deleted file mode 100644 index e5e2dcc23..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/styles/globals.css +++ /dev/null @@ -1,16 +0,0 @@ -html, -body { - padding: 0; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} - -a { - color: inherit; - text-decoration: none; -} - -* { - box-sizing: border-box; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/query-client.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/query-client.ts deleted file mode 100644 index bda64397c..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/query-client.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - defaultShouldDehydrateQuery, - QueryClient, -} from "@tanstack/react-query"; -import SuperJSON from "superjson"; - -export const createQueryClient = () => - new QueryClient({ - defaultOptions: { - queries: { - // With SSR, we usually want to set some default staleTime - // above 0 to avoid refetching immediately on the client - staleTime: 30 * 1000, - }, - dehydrate: { - serializeData: SuperJSON.serialize, - shouldDehydrateQuery: (query) => - defaultShouldDehydrateQuery(query) || - query.state.status === "pending", - }, - hydrate: { - deserializeData: SuperJSON.deserialize, - }, - }, - }); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/react.tsx b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/react.tsx deleted file mode 100644 index be9631708..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/react.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; - -import { QueryClientProvider, type QueryClient } from '@tanstack/react-query'; -import { loggerLink, unstable_httpBatchStreamLink } from '@trpc/client'; -import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; -import { useState } from 'react'; -import SuperJSON from 'superjson'; - -import { type AppRouter } from '~/server/api/root'; -import { createQueryClient } from './query-client'; -import { createTRPCReact } from '../server/api/routers/generated/client/react'; - -let clientQueryClientSingleton: QueryClient | undefined = undefined; -const getQueryClient = () => { - if (typeof window === 'undefined') { - // Server: always make a new query client - return createQueryClient(); - } - // Browser: use singleton pattern to keep the same query client - return (clientQueryClientSingleton ??= createQueryClient()); -}; - -export const api = createTRPCReact(); - -/** - * Inference helper for inputs. - * - * @example type HelloInput = RouterInputs['example']['hello'] - */ -export type RouterInputs = inferRouterInputs; - -/** - * Inference helper for outputs. - * - * @example type HelloOutput = RouterOutputs['example']['hello'] - */ -export type RouterOutputs = inferRouterOutputs; - -export function TRPCReactProvider(props: { children: React.ReactNode }) { - const queryClient = getQueryClient(); - - const [trpcClient] = useState(() => - api.createClient({ - links: [ - loggerLink({ - enabled: (op) => - process.env.NODE_ENV === 'development' || - (op.direction === 'down' && op.result instanceof Error), - }), - unstable_httpBatchStreamLink({ - transformer: SuperJSON, - url: getBaseUrl() + '/api/trpc', - headers: () => { - const headers = new Headers(); - headers.set('x-trpc-source', 'nextjs-react'); - return headers; - }, - }), - ], - }) - ); - - return ( - - - {props.children} - - - ); -} - -function getBaseUrl() { - if (typeof window !== 'undefined') return window.location.origin; - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; - return `http://localhost:${process.env.PORT ?? 3000}`; -} diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/server.ts b/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/server.ts deleted file mode 100644 index 59300a638..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/src/trpc/server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import "server-only"; - -import { createHydrationHelpers } from "@trpc/react-query/rsc"; -import { headers } from "next/headers"; -import { cache } from "react"; - -import { createCaller, type AppRouter } from "~/server/api/root"; -import { createTRPCContext } from "~/server/api/trpc"; -import { createQueryClient } from "./query-client"; - -/** - * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when - * handling a tRPC call from a React Server Component. - */ -const createContext = cache(() => { - const heads = new Headers(headers()); - heads.set("x-trpc-source", "rsc"); - - return createTRPCContext({ - headers: heads, - }); -}); - -const getQueryClient = cache(createQueryClient); -const caller = createCaller(createContext); - -export const { trpc: api, HydrateClient } = createHydrationHelpers( - caller, - getQueryClient -); diff --git a/packages/plugins/trpc/tests/projects/t3-trpc-v11/tsconfig.json b/packages/plugins/trpc/tests/projects/t3-trpc-v11/tsconfig.json deleted file mode 100644 index 905062ded..000000000 --- a/packages/plugins/trpc/tests/projects/t3-trpc-v11/tsconfig.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "compilerOptions": { - /* Base Options: */ - "esModuleInterop": true, - "skipLibCheck": true, - "target": "es2022", - "allowJs": true, - "resolveJsonModule": true, - "moduleDetection": "force", - "isolatedModules": true, - - /* Strictness */ - "strict": true, - "noUncheckedIndexedAccess": true, - "checkJs": true, - - /* Bundled projects */ - "lib": ["dom", "dom.iterable", "ES2022"], - "noEmit": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "jsx": "preserve", - "plugins": [{ "name": "next" }], - "incremental": true, - - /* Path Aliases */ - "baseUrl": ".", - "paths": { - "~/*": ["./src/*"] - } - }, - "include": [ - ".eslintrc.cjs", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.cjs", - "**/*.js", - ".next/types/**/*.ts" - ], - "exclude": ["node_modules"] -} diff --git a/packages/plugins/trpc/tests/t3.test.ts b/packages/plugins/trpc/tests/t3.test.ts deleted file mode 100644 index 6a7843f11..000000000 --- a/packages/plugins/trpc/tests/t3.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -import { run } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('tRPC plugin tests with create-t3-app', () => { - let origDir: string | undefined; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - if (origDir) { - process.chdir(origDir); - } - }); - - it('project test trpc v10', () => { - const ver = require(path.join(__dirname, '../package.json')).version; - process.chdir(path.join(__dirname, './projects/t3-trpc-v10')); - - const deps = ['zenstackhq-language', 'zenstackhq-runtime', 'zenstackhq-sdk', 'zenstack']; - for (const dep of deps) { - run(`npm install ${path.join(__dirname, '../../../../.build/') + dep + '-' + ver + '.tgz'}`); - } - - run('npx zenstack generate'); - run('npm run build'); - }); - - it('project test trpc v11', () => { - const ver = require(path.join(__dirname, '../package.json')).version; - process.chdir(path.join(__dirname, './projects/t3-trpc-v11')); - - const deps = ['zenstackhq-language', 'zenstackhq-runtime', 'zenstackhq-sdk', 'zenstack']; - for (const dep of deps) { - run(`npm install ${path.join(__dirname, '../../../../.build/') + dep + '-' + ver + '.tgz'}`); - } - - run('npx zenstack generate'); - run('npm run build'); - }); -}); diff --git a/packages/plugins/trpc/tests/trpc.test.ts b/packages/plugins/trpc/tests/trpc.test.ts deleted file mode 100644 index 35ef8e503..000000000 --- a/packages/plugins/trpc/tests/trpc.test.ts +++ /dev/null @@ -1,483 +0,0 @@ -/// - -import { loadSchema, normalizePath } from '@zenstackhq/testtools'; -import fs from 'fs'; -import path from 'path'; -import tmp from 'tmp'; - -describe('tRPC Plugin Tests', () => { - let origDir: string; - - beforeAll(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('run plugin absolute output', async () => { - await loadSchema( - ` -plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/trpc' -} - -model User { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - role role @default(USER) - posts post_Item[] -} - -enum role { - USER - ADMIN -} - -model post_Item { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - viewCount Int @default(0) -} - -model Foo { - id String @id - @@ignore -} - `, - { - provider: 'postgresql', - pushDb: false, - extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client@10', '@trpc/server@10'], - compile: true, - fullZod: true, - } - ); - }); - - it('run plugin relative output', async () => { - const { projectDir } = await loadSchema( - ` -plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = './trpc' -} - -model User { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - role String @default('USER') - posts Post[] -} - -model Post { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - viewCount Int @default(0) -} - -model Foo { - id String @id - @@ignore -} - `, - { - pushDb: false, - extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client@10', '@trpc/server@10'], - compile: true, - fullZod: true, - } - ); - expect(fs.existsSync(path.join(projectDir, 'trpc'))).toBe(true); - }); - - it('run plugin non-standard zmodel location', async () => { - const { projectDir } = await loadSchema( - ` -plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = './trpc' -} - -model User { - id String @id - posts Post[] -} - -model Post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? -} - `, - { - pushDb: false, - extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client@10', '@trpc/server@10'], - compile: true, - fullZod: true, - customSchemaFilePath: 'zenstack/schema.zmodel', - } - ); - expect(fs.existsSync(path.join(projectDir, 'zenstack/trpc'))).toBe(true); - }); - - it('generateModelActions option string', async () => { - const { projectDir } = await loadSchema( - ` -plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = './trpc' - generateModelActions = 'findMany,findUnique,update' -} - -model Post { - id String @id - title String -} - `, - { - pushDb: false, - extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client@10', '@trpc/server@10'], - compile: true, - fullZod: true, - customSchemaFilePath: 'zenstack/schema.zmodel', - } - ); - const content = fs.readFileSync(path.join(projectDir, 'zenstack/trpc/routers/Post.router.ts'), 'utf-8'); - expect(content).toContain('findMany:'); - expect(content).toContain('findUnique:'); - expect(content).toContain('update:'); - expect(content).not.toContain('create:'); - expect(content).not.toContain('aggregate:'); - }); - - it('generateModelActions option array', async () => { - const { projectDir } = await loadSchema( - ` -plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = './trpc' - generateModelActions = ['findMany', 'findUnique', 'update'] -} - -model Post { - id String @id - title String -} - `, - { - pushDb: false, - extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client@10', '@trpc/server@10'], - compile: true, - fullZod: true, - customSchemaFilePath: 'zenstack/schema.zmodel', - } - ); - const content = fs.readFileSync(path.join(projectDir, 'zenstack/trpc/routers/Post.router.ts'), 'utf-8'); - expect(content).toContain('findMany:'); - expect(content).toContain('findUnique:'); - expect(content).toContain('update:'); - expect(content).not.toContain('create:'); - expect(content).not.toContain('aggregate:'); - }); - - const BLOG_BASE_SCHEMA = ` - model User { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - posts Post[] - } - - model Post { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - } - `; - - it('generate client helper react-query', async () => { - await loadSchema( - ` - plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/trpc' - generateClientHelpers = 'react' - } - - ${BLOG_BASE_SCHEMA} - `, - { - pushDb: false, - extraDependencies: [ - path.resolve(__dirname, '../dist'), - '@trpc/client@10', - '@trpc/server@10', - '@trpc/react-query@10', - ], - compile: true, - fullZod: true, - } - ); - }); - - it('generate client helper next', async () => { - await loadSchema( - ` - plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/trpc' - generateClientHelpers = 'next' - } - - ${BLOG_BASE_SCHEMA} - `, - { - pushDb: false, - extraDependencies: [ - path.resolve(__dirname, '../dist'), - '@trpc/client@10', - '@trpc/server@10', - '@trpc/next@10', - ], - compile: true, - fullZod: true, - } - ); - }); - - it('mixed casing', async () => { - await loadSchema( - ` -plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/trpc' -} - -model User { - id String @id - email String @unique - posts post_item[] -} - -model post_item { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? -} - `, - { - pushDb: false, - extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client@10', '@trpc/server@10'], - compile: true, - fullZod: true, - } - ); - }); - - it('generate for selected models and actions', async () => { - const { projectDir } = await loadSchema( - ` -datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') -} - -generator js { - provider = 'prisma-client-js' -} - -plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/trpc' - generateModels = ['Post'] - generateModelActions = ['findMany', 'update'] -} - -model User { - id String @id - email String @unique - posts Post[] -} - -model Post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? -} - -model Foo { - id String @id - value Int -} - `, - { - addPrelude: false, - pushDb: false, - extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client@10', '@trpc/server@10'], - compile: true, - } - ); - - expect(fs.existsSync(path.join(projectDir, 'trpc/routers/User.router.ts'))).toBeFalsy(); - expect(fs.existsSync(path.join(projectDir, 'trpc/routers/Foo.router.ts'))).toBeFalsy(); - expect(fs.existsSync(path.join(projectDir, 'trpc/routers/Post.router.ts'))).toBeTruthy(); - - const postRouterContent = fs.readFileSync(path.join(projectDir, 'trpc/routers/Post.router.ts'), 'utf8'); - expect(postRouterContent).toContain('findMany:'); - expect(postRouterContent).toContain('update:'); - expect(postRouterContent).not.toContain('findUnique:'); - expect(postRouterContent).not.toContain('create:'); - - // trpc plugin passes "generateModels" option down to implicitly enabled zod plugin - - expect( - fs.existsSync(path.join(projectDir, 'node_modules/.zenstack/zod/input/PostInput.schema.js')) - ).toBeTruthy(); - // zod for User is generated due to transitive dependency - expect( - fs.existsSync(path.join(projectDir, 'node_modules/.zenstack/zod/input/UserInput.schema.js')) - ).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'node_modules/.zenstack/zod/input/FooInput.schema.js'))).toBeFalsy(); - }); - - it('generate for selected models with zod plugin declared', async () => { - const { projectDir } = await loadSchema( - ` -datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') -} - -generator js { - provider = 'prisma-client-js' -} - -plugin zod { - provider = '@core/zod' -} - -plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/trpc' - generateModels = ['Post'] - generateModelActions = ['findMany', 'update'] -} - -model User { - id String @id - email String @unique - posts Post[] -} - -model Post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? -} - -model Foo { - id String @id - value Int -} - `, - { - addPrelude: false, - pushDb: false, - extraDependencies: [path.resolve(__dirname, '../dist'), '@trpc/client@10', '@trpc/server@10'], - compile: true, - } - ); - - // trpc plugin's "generateModels" shouldn't interfere in this case - - expect( - fs.existsSync(path.join(projectDir, 'node_modules/.zenstack/zod/input/PostInput.schema.js')) - ).toBeTruthy(); - expect( - fs.existsSync(path.join(projectDir, 'node_modules/.zenstack/zod/input/UserInput.schema.js')) - ).toBeTruthy(); - expect( - fs.existsSync(path.join(projectDir, 'node_modules/.zenstack/zod/input/FooInput.schema.js')) - ).toBeTruthy(); - }); - - it('clear output', async () => { - const { name: projectDir } = tmp.dirSync(); - fs.mkdirSync(path.join(projectDir, 'trpc'), { recursive: true }); - fs.writeFileSync(path.join(projectDir, 'trpc', 'test.txt'), 'hello'); - - await loadSchema( - ` - plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/trpc' - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique - password String @omit - } - `, - { - pushDb: false, - projectDir, - extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`], - } - ); - - expect(fs.existsSync(path.join(projectDir, 'trpc', 'test.txt'))).toBeFalsy(); - }); - - it('existing output as file', async () => { - const { name: projectDir } = tmp.dirSync(); - fs.writeFileSync(path.join(projectDir, 'trpc'), 'hello'); - - await expect( - loadSchema( - ` - plugin trpc { - provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' - output = '$projectRoot/trpc' - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String - password String @omit - } - `, - { pushDb: false, projectDir, extraDependencies: [`${normalizePath(path.join(__dirname, '../dist'))}`] } - ) - ).rejects.toThrow('already exists and is not a directory'); - }); -}); diff --git a/packages/plugins/trpc/tsconfig.json b/packages/plugins/trpc/tsconfig.json deleted file mode 100644 index 4291db7df..000000000 --- a/packages/plugins/trpc/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "lib": ["ESNext", "DOM"], - "outDir": "dist" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/runtime/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/runtime/LICENSE b/packages/runtime/LICENSE deleted file mode 120000 index 30cff7403..000000000 --- a/packages/runtime/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/runtime/README.md b/packages/runtime/README.md deleted file mode 100644 index a5cd746fa..000000000 --- a/packages/runtime/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ZenStack Runtime Library - -This package is the runtime library supporting web apps built using ZenStack. - -Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/runtime/jest.config.ts b/packages/runtime/jest.config.ts deleted file mode 120000 index a12d812fd..000000000 --- a/packages/runtime/jest.config.ts +++ /dev/null @@ -1 +0,0 @@ -../../jest.config.ts \ No newline at end of file diff --git a/packages/runtime/package.json b/packages/runtime/package.json deleted file mode 100644 index 0060e6693..000000000 --- a/packages/runtime/package.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "name": "@zenstackhq/runtime", - "displayName": "ZenStack Runtime Library", - "version": "2.22.2", - "description": "Runtime of ZenStack for both client-side and server-side environments.", - "repository": { - "type": "git", - "url": "https://github.com/zenstackhq/zenstack" - }, - "scripts": { - "clean": "rimraf dist", - "build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup-browser.config.ts && tsup-node --config ./tsup-cross.config.ts && copyfiles ./package.json ./README.md ../../LICENSE dist && copyfiles -u1 \"res/**/*\" dist && pnpm pack dist --pack-destination ../../../.build", - "watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup-browser.config.ts --watch\" \"tsup-node --config ./tsup-cross.config.ts --watch\"", - "lint": "eslint src --ext ts", - "prepublishOnly": "pnpm build" - }, - "main": "index.js", - "types": "index.d.ts", - "exports": { - ".": { - "types": "./index.d.ts", - "default": "./index.js" - }, - "./local-helpers": { - "types": "./local-helpers/index.d.ts", - "default": "./local-helpers/index.js" - }, - "./edge": { - "types": "./edge.d.ts", - "default": "./edge.js" - }, - "./enhancements/node": { - "types": "./enhancements/node/index.d.ts", - "default": "./enhancements/node/index.js" - }, - "./enhancements/edge": { - "types": "./enhancements/edge/index.d.ts", - "default": "./enhancements/edge/index.js" - }, - "./validation": { - "types": "./validation.d.ts", - "default": "./validation.js" - }, - "./constraint-solver": { - "types": "./constraint-solver.d.ts", - "default": "./constraint-solver.js" - }, - "./zod": { - "types": "./zod/index.d.ts", - "default": "./zod/index.js" - }, - "./zod/input": { - "types": "./zod/input.d.ts", - "default": "./zod/input.js" - }, - "./zod/models": { - "types": "./zod/models.d.ts", - "default": "./zod/models.js" - }, - "./zod/objects": { - "types": "./zod/objects.d.ts", - "default": "./zod/objects.js" - }, - "./browser": { - "types": "./browser/index.d.ts", - "import": "./browser/index.mjs", - "require": "./browser/index.js", - "default": "./browser/index.js" - }, - "./cross": { - "types": "./cross/index.d.ts", - "import": "./cross/index.mjs", - "require": "./cross/index.js", - "default": "./cross/index.js" - }, - "./model-meta": { - "types": "./model-meta.d.ts", - "default": "./model-meta.js" - }, - "./models": { - "types": "./models.d.ts", - "default": "./models.js" - }, - "./zod-utils": { - "types": "./zod-utils.d.ts", - "default": "./zod-utils.js" - }, - "./encryption": { - "types": "./encryption/index.d.ts", - "default": "./encryption/index.js" - }, - "./package.json": { - "default": "./package.json" - } - }, - "publishConfig": { - "directory": "dist", - "linkDirectory": true - }, - "dependencies": { - "bcryptjs": "^2.4.3", - "buffer": "^6.0.3", - "decimal.js-light": "^2.5.1", - "deepmerge": "^4.3.1", - "logic-solver": "^2.0.1", - "pluralize": "^8.0.0", - "safe-json-stringify": "^1.2.0", - "semver": "^7.5.2", - "superjson": "^1.13.0", - "ts-pattern": "^4.3.0", - "tslib": "^2.4.1", - "uuid": "^9.0.0", - "zod-validation-error": "catalog:" - }, - "devDependencies": { - "@types/bcryptjs": "^2.4.2", - "@types/pluralize": "^0.0.29", - "@types/safe-json-stringify": "^1.1.5", - "@types/semver": "^7.3.13", - "@types/uuid": "^8.3.4", - "decimal.js-light": "^2.5.1", - "superjson": "^1.13.0", - "uuid": "^9.0.0", - "zod": "^3.25.0", - "@prisma/client": "6.19.x" - }, - "peerDependencies": { - "@prisma/client": "5.0.0 - 7.x", - "zod": "catalog:" - }, - "author": { - "name": "ZenStack Team" - }, - "homepage": "https://zenstack.dev", - "license": "MIT" -} diff --git a/packages/runtime/res/enhance-edge.d.ts b/packages/runtime/res/enhance-edge.d.ts deleted file mode 100644 index 7f165c4d4..000000000 --- a/packages/runtime/res/enhance-edge.d.ts +++ /dev/null @@ -1 +0,0 @@ -export { auth, enhance, type PrismaClient } from '.zenstack/enhance-edge'; diff --git a/packages/runtime/res/enhance-edge.js b/packages/runtime/res/enhance-edge.js deleted file mode 100644 index 7c716a79a..000000000 --- a/packages/runtime/res/enhance-edge.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); - -try { - exports.enhance = require('.zenstack/enhance-edge').enhance; -} catch { - exports.enhance = function () { - throw new Error('Generated "enhance" function not found. Please run `zenstack generate` first.'); - }; -} diff --git a/packages/runtime/res/enhance.d.ts b/packages/runtime/res/enhance.d.ts deleted file mode 100644 index 3058f736e..000000000 --- a/packages/runtime/res/enhance.d.ts +++ /dev/null @@ -1 +0,0 @@ -export { auth, enhance, type PrismaClient, type Enhanced } from '.zenstack/enhance'; diff --git a/packages/runtime/res/enhance.js b/packages/runtime/res/enhance.js deleted file mode 100644 index aa19af865..000000000 --- a/packages/runtime/res/enhance.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); - -try { - exports.enhance = require('.zenstack/enhance').enhance; -} catch { - exports.enhance = function () { - throw new Error('Generated "enhance" function not found. Please run `zenstack generate` first.'); - }; -} diff --git a/packages/runtime/res/model-meta.d.ts b/packages/runtime/res/model-meta.d.ts deleted file mode 100644 index fbda166b2..000000000 --- a/packages/runtime/res/model-meta.d.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '.zenstack/model-meta'; diff --git a/packages/runtime/res/model-meta.js b/packages/runtime/res/model-meta.js deleted file mode 100644 index d4af2b522..000000000 --- a/packages/runtime/res/model-meta.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); - -try { - exports.default = require('.zenstack/model-meta').default; -} catch { - exports.default = function () { - throw new Error('Generated model meta not found. Please run `zenstack generate` first.'); - }; -} diff --git a/packages/runtime/res/models.d.ts b/packages/runtime/res/models.d.ts deleted file mode 100644 index 9f02a1b29..000000000 --- a/packages/runtime/res/models.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '.zenstack/models'; diff --git a/packages/runtime/res/models.js b/packages/runtime/res/models.js deleted file mode 100644 index 1e7286d53..000000000 --- a/packages/runtime/res/models.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('.zenstack/models'); diff --git a/packages/runtime/res/zod/index.d.ts b/packages/runtime/res/zod/index.d.ts deleted file mode 100644 index 748da4391..000000000 --- a/packages/runtime/res/zod/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as models from './models'; -export * as input from './input'; -export * as objects from './objects'; diff --git a/packages/runtime/res/zod/index.js b/packages/runtime/res/zod/index.js deleted file mode 100644 index 98ee4ea4f..000000000 --- a/packages/runtime/res/zod/index.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - models: require('./models'), - input: require('./input'), - objects: require('./objects') -}; diff --git a/packages/runtime/res/zod/input.d.ts b/packages/runtime/res/zod/input.d.ts deleted file mode 100644 index 3768bcf6a..000000000 --- a/packages/runtime/res/zod/input.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '.zenstack/zod/input'; diff --git a/packages/runtime/res/zod/input.js b/packages/runtime/res/zod/input.js deleted file mode 100644 index 57b6faebb..000000000 --- a/packages/runtime/res/zod/input.js +++ /dev/null @@ -1,8 +0,0 @@ -let schemas; -try { - schemas = require('.zenstack/zod/input'); -} catch {} - -module.exports = schemas && { - ...schemas, -}; diff --git a/packages/runtime/res/zod/models.d.ts b/packages/runtime/res/zod/models.d.ts deleted file mode 100644 index a5a7395ac..000000000 --- a/packages/runtime/res/zod/models.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '.zenstack/zod/models'; diff --git a/packages/runtime/res/zod/models.js b/packages/runtime/res/zod/models.js deleted file mode 100644 index df1a370b4..000000000 --- a/packages/runtime/res/zod/models.js +++ /dev/null @@ -1,8 +0,0 @@ -let schemas; -try { - schemas = require('.zenstack/zod/models'); -} catch {} - -module.exports = schemas && { - ...schemas, -}; diff --git a/packages/runtime/res/zod/objects.d.ts b/packages/runtime/res/zod/objects.d.ts deleted file mode 100644 index a613e84c3..000000000 --- a/packages/runtime/res/zod/objects.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '.zenstack/zod/objects'; diff --git a/packages/runtime/res/zod/objects.js b/packages/runtime/res/zod/objects.js deleted file mode 100644 index 067e3a8b6..000000000 --- a/packages/runtime/res/zod/objects.js +++ /dev/null @@ -1,8 +0,0 @@ -let schemas; -try { - schemas = require('.zenstack/zod/objects'); -} catch {} - -module.exports = schemas && { - ...schemas, -}; diff --git a/packages/runtime/src/browser/index.ts b/packages/runtime/src/browser/index.ts deleted file mode 100644 index 90e875c0a..000000000 --- a/packages/runtime/src/browser/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './serialization'; diff --git a/packages/runtime/src/browser/serialization.ts b/packages/runtime/src/browser/serialization.ts deleted file mode 100644 index 478bd66a9..000000000 --- a/packages/runtime/src/browser/serialization.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Buffer } from 'buffer'; -import Decimal from 'decimal.js-light'; -import SuperJSON from 'superjson'; - -SuperJSON.registerCustom( - { - isApplicable: (v): v is Decimal => - v instanceof Decimal || - // interop with decimal.js - v?.toStringTag === '[object Decimal]', - serialize: (v) => v.toJSON(), - deserialize: (v) => new Decimal(v), - }, - 'Decimal' -); - -SuperJSON.registerCustom( - { - isApplicable: (v): v is Buffer => Buffer.isBuffer(v), - serialize: (v) => v.toString('base64'), - deserialize: (v) => Buffer.from(v, 'base64'), - }, - 'Bytes' -); - -/** - * Serialize the given value with superjson - */ -export function serialize(value: unknown): { data: unknown; meta: unknown } { - const { json, meta } = SuperJSON.serialize(value); - return { data: json, meta }; -} - -/** - * Deserialize the given value with superjson using the given metadata - */ -export function deserialize(value: unknown, meta: any): unknown { - return SuperJSON.deserialize({ json: value as any, meta }); -} diff --git a/packages/runtime/src/constants.ts b/packages/runtime/src/constants.ts deleted file mode 100644 index b4fd9204c..000000000 --- a/packages/runtime/src/constants.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Default path for loading CLI-generated code - */ -export const DEFAULT_RUNTIME_LOAD_PATH = '.zenstack'; - -/** - * Default length of password hash salt (used by bcryptjs to hash password) - */ -export const DEFAULT_PASSWORD_SALT_LENGTH = 12; - -/** - * Reasons for a CRUD operation to fail - */ -export enum CrudFailureReason { - /** - * CRUD failed because of access policy violation. - */ - ACCESS_POLICY_VIOLATION = 'ACCESS_POLICY_VIOLATION', - - /** - * CRUD succeeded but the result was not readable. - */ - RESULT_NOT_READABLE = 'RESULT_NOT_READABLE', - - /** - * CRUD failed because of a data validation rule violation. - */ - DATA_VALIDATION_VIOLATION = 'DATA_VALIDATION_VIOLATION', -} - -/** - * Prisma error codes used - */ -export enum PrismaErrorCode { - /** - * Unique constraint failed - */ - UNIQUE_CONSTRAINT_FAILED = 'P2002', - - /** - * A constraint failed on the database - */ - CONSTRAINT_FAILED = 'P2004', - - /** - * The required connected records were not found - */ - REQUIRED_CONNECTED_RECORD_NOT_FOUND = 'P2018', - - /** - * An operation failed because it depends on one or more records that were required but not found - */ - DEPEND_ON_RECORD_NOT_FOUND = 'P2025', -} - -/** - * Field name for getting current enhancer - */ -export const PRISMA_PROXY_ENHANCER = '$__zenstack_enhancer'; - -/** - * Minimum Prisma version supported - */ -export const PRISMA_MINIMUM_VERSION = '5.0.0'; - -/** - * Prefix for auxiliary relation field generated for delegated models - */ -export const DELEGATE_AUX_RELATION_PREFIX = 'delegate_aux'; - -/** - * Prisma actions that can have a write payload - */ -export const ACTIONS_WITH_WRITE_PAYLOAD = [ - 'create', - 'createMany', - 'createManyAndReturn', - 'update', - 'updateMany', - 'updateManyAndReturn', - 'upsert', -]; diff --git a/packages/runtime/src/cross/clone.ts b/packages/runtime/src/cross/clone.ts deleted file mode 100644 index 4ed5d8409..000000000 --- a/packages/runtime/src/cross/clone.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { isPlainObject } from '../local-helpers'; - -/** - * Clones the given object. Only arrays and plain objects are cloned. Other values are returned as is. - */ -export function clone(value: T): T { - if (Array.isArray(value)) { - return value.map((v) => clone(v)) as T; - } - - if (typeof value === 'object') { - if (!value || !isPlainObject(value)) { - 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]); - } - return result; - } - - return value; -} diff --git a/packages/runtime/src/cross/index.ts b/packages/runtime/src/cross/index.ts deleted file mode 100644 index 84d23cd5d..000000000 --- a/packages/runtime/src/cross/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './clone'; -export * from './model-data-visitor'; -export * from './model-meta'; -export * from './mutator'; -export * from './nested-read-visitor'; -export * from './nested-write-visitor'; -export * from './query-analyzer'; -export * from './types'; -export * from './utils'; diff --git a/packages/runtime/src/cross/model-data-visitor.ts b/packages/runtime/src/cross/model-data-visitor.ts deleted file mode 100644 index 543932521..000000000 --- a/packages/runtime/src/cross/model-data-visitor.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { resolveField, type ModelMeta } from '.'; - -/** - * Callback for @see ModelDataVisitor. - */ -export type ModelDataVisitorCallback = (model: string, data: any, scalarData: any) => void; - -/** - * Visitor that traverses data returned by a Prisma query. - */ -export class ModelDataVisitor { - constructor(private modelMeta: ModelMeta) {} - - /** - * Visits the given model data. - */ - visit(model: string, data: any, callback: ModelDataVisitorCallback) { - if (!data || typeof data !== 'object') { - return; - } - - const scalarData: Record = {}; - const subTasks: Array<{ model: string; data: any }> = []; - - for (const [k, v] of Object.entries(data)) { - const field = resolveField(this.modelMeta, model, k); - if (field && field.isDataModel) { - if (field.isArray && Array.isArray(v)) { - subTasks.push(...v.map((item) => ({ model: field.type, data: item }))); - } else { - subTasks.push({ model: field.type, data: v }); - } - } else { - scalarData[k] = v; - } - } - - callback(model, data, scalarData); - subTasks.forEach(({ model, data }) => this.visit(model, data, callback)); - } -} diff --git a/packages/runtime/src/cross/model-meta.ts b/packages/runtime/src/cross/model-meta.ts deleted file mode 100644 index 805b3d6d1..000000000 --- a/packages/runtime/src/cross/model-meta.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { lowerCaseFirst } from '../local-helpers'; - -/** - * Runtime information of a data model or field attribute - */ -export type RuntimeAttribute = { - /** - * Attribute name - */ - name: string; - - /** - * Attribute arguments - */ - args: Array<{ name?: string; value?: unknown }>; -}; - -/** - * Function for computing default value for a field - */ -export type FieldDefaultValueProvider = (userContext: unknown) => unknown; - -/** - * Action to take when the related model is deleted or updated - */ -export type RelationAction = 'Cascade' | 'Restrict' | 'NoAction' | 'SetNull' | 'SetDefault'; - -/** - * Runtime information of a data model field - */ -export type FieldInfo = { - /** - * Field name - */ - name: string; - - /** - * Field type name - */ - type: string; - - /** - * If the field is an ID field or part of a multi-field ID - */ - isId?: boolean; - - /** - * If the field type is a data model (or an optional/array of data model) - */ - isDataModel?: boolean; - - /** - * If the field type is a type def (or an optional/array of type def) - */ - isTypeDef?: boolean; - - /** - * If the field is an array - */ - isArray?: boolean; - - /** - * If the field is optional - */ - isOptional?: boolean; - - /** - * Attributes on the field - */ - attributes?: RuntimeAttribute[]; - - /** - * If the field is a relation field, the field name of the reverse side of the relation - */ - backLink?: string; - - /** - * If the field is the owner side of a relation - */ - isRelationOwner?: boolean; - - /** - * Action to take when the related model is deleted. - */ - onDeleteAction?: RelationAction; - - /** - * Action to take when the related model is updated. - */ - onUpdateAction?: RelationAction; - - /** - * If the field is a foreign key field - */ - isForeignKey?: boolean; - - /** - * If the field is a foreign key field, the field name of the corresponding relation field. - * Only available on foreign key fields. - */ - relationField?: string; - - /** - * Mapping from relation's pk to fk. Only available on relation fields. - */ - foreignKeyMapping?: Record; - - /** - * Model from which the field is inherited - */ - inheritedFrom?: string; - - /** - * A function that provides a default value for the field - */ - defaultValueProvider?: FieldDefaultValueProvider; - - /** - * If the field is an auto-increment field - */ - isAutoIncrement?: boolean; -}; - -/** - * Metadata for a model-level unique constraint - * e.g.: @@unique([a, b]) - */ -export type UniqueConstraint = { name: string; fields: string[] }; - -/** - * Metadata for a data model - */ -export type ModelInfo = { - /** - * Model name - */ - name: string; - - /** - * Base types (not including abstract base models). - */ - baseTypes?: string[]; - - /** - * Fields - */ - fields: Record; - - /** - * Unique constraints - */ - uniqueConstraints?: Record; - - /** - * Attributes on the model - */ - attributes?: RuntimeAttribute[]; - - /** - * Discriminator field name - */ - discriminator?: string; -}; - -/** - * Metadata for a type def - */ -export type TypeDefInfo = { - /** - * TypeDef name - */ - name: string; - - /** - * Fields - */ - fields: Record; -}; - -/** - * ZModel data model metadata - */ -export type ModelMeta = { - /** - * Data models - */ - models: Record; - - /** - * Type defs - */ - typeDefs?: Record; - - /** - * Mapping from model name to models that will be deleted because of it due to cascade delete - */ - deleteCascade?: Record; - - /** - * Name of model that backs the `auth()` function - */ - authModel?: string; - - /** - * Optional map from full names to shortened names, used for extra fields/relations generated by ZenStack - */ - shortNameMap?: Record; -}; - -/** - * Resolves a model field to its metadata. Returns undefined if not found. - */ -export function resolveField( - modelMeta: ModelMeta, - modelOrTypeDef: string, - field: string, - isTypeDef = false -): FieldInfo | undefined { - const container = isTypeDef ? modelMeta.typeDefs : modelMeta.models; - return container?.[lowerCaseFirst(modelOrTypeDef)]?.fields?.[field]; -} - -/** - * Resolves a model field to its metadata. Throws an error if not found. - */ -export function requireField(modelMeta: ModelMeta, model: string, field: string, isTypeDef = false) { - const f = resolveField(modelMeta, model, field, isTypeDef); - if (!f) { - throw new Error(`Field ${model}.${field} cannot be resolved`); - } - return f; -} - -/** - * Gets all fields of a model. - */ -export function getFields(modelMeta: ModelMeta, model: string) { - return modelMeta.models[lowerCaseFirst(model)]?.fields; -} - -/** - * Gets unique constraints of a model. - */ -export function getUniqueConstraints(modelMeta: ModelMeta, model: string) { - return modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints; -} diff --git a/packages/runtime/src/cross/mutator.ts b/packages/runtime/src/cross/mutator.ts deleted file mode 100644 index af56e6cdd..000000000 --- a/packages/runtime/src/cross/mutator.ts +++ /dev/null @@ -1,448 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { v4 as uuid } from 'uuid'; -import { - FieldInfo, - NestedWriteVisitor, - enumerate, - getFields, - getIdFields, - type ModelMeta, - type PrismaWriteActionType, -} from '.'; -import { clone } from './clone'; - -/** - * Tries to apply a mutation to a query result. - * - * @param queryModel the model of the query - * @param queryOp the operation of the query - * @param queryData the result data of the query - * @param mutationModel the model of the mutation - * @param mutationOp the operation of the mutation - * @param mutationArgs the arguments of the mutation - * @param modelMeta the model metadata - * @param logging whether to log the mutation application - * @returns the updated query data if the mutation is applicable, otherwise undefined - */ -export async function applyMutation( - queryModel: string, - queryOp: string, - queryData: any, - mutationModel: string, - mutationOp: PrismaWriteActionType, - mutationArgs: any, - modelMeta: ModelMeta, - logging: boolean -) { - if (!queryData || (typeof queryData !== 'object' && !Array.isArray(queryData))) { - return undefined; - } - - if (!queryOp.startsWith('find')) { - // only findXXX results are applicable - return undefined; - } - - return await doApplyMutation(queryModel, queryData, mutationModel, mutationOp, mutationArgs, modelMeta, logging); -} - -async function doApplyMutation( - queryModel: string, - queryData: any, - mutationModel: string, - mutationOp: PrismaWriteActionType, - mutationArgs: any, - modelMeta: ModelMeta, - logging: boolean -) { - let resultData = queryData; - let updated = false; - - const visitor = new NestedWriteVisitor(modelMeta, { - create: (model, args) => { - if ( - model === queryModel && - Array.isArray(resultData) // "create" mutation is only relevant for arrays - ) { - const r = createMutate(queryModel, resultData, args, modelMeta, logging); - if (r) { - resultData = r; - updated = true; - } - } - }, - - createMany: (model, args) => { - if ( - model === queryModel && - args?.data && - Array.isArray(resultData) // "createMany" mutation is only relevant for arrays - ) { - for (const oneArg of enumerate(args.data)) { - const r = createMutate(queryModel, resultData, oneArg, modelMeta, logging); - if (r) { - resultData = r; - updated = true; - } - } - } - }, - - update: (model, args) => { - if ( - model === queryModel && - !Array.isArray(resultData) // array elements will be handled with recursion - ) { - const r = updateMutate(queryModel, resultData, model, args, modelMeta, logging); - if (r) { - resultData = r; - updated = true; - } - } - }, - - upsert: (model, args) => { - if (model === queryModel && args?.where && args?.create && args?.update) { - const r = upsertMutate(queryModel, resultData, model, args, modelMeta, logging); - if (r) { - resultData = r; - updated = true; - } - } - }, - - delete: (model, args) => { - if (model === queryModel) { - const r = deleteMutate(queryModel, resultData, model, args, modelMeta, logging); - if (r) { - resultData = r; - updated = true; - } - } - }, - }); - - await visitor.visit(mutationModel, mutationOp, mutationArgs); - - const modelFields = getFields(modelMeta, queryModel); - - if (Array.isArray(resultData)) { - // try to apply mutation to each item in the array, replicate the entire - // array if any item is updated - - let arrayCloned = false; - for (let i = 0; i < resultData.length; i++) { - const item = resultData[i]; - if ( - !item || - typeof item !== 'object' || - item.$optimistic // skip items already optimistically updated - ) { - continue; - } - - const r = await doApplyMutation( - queryModel, - item, - mutationModel, - mutationOp, - mutationArgs, - modelMeta, - logging - ); - - if (r && typeof r === 'object') { - if (!arrayCloned) { - resultData = [...resultData]; - arrayCloned = true; - } - resultData[i] = r; - updated = true; - } - } - } else if (resultData !== null && typeof resultData === 'object') { - // Clone resultData to prevent mutations affecting the loop - const currentData = { ...resultData }; - - // iterate over each field and apply mutation to nested data models - for (const [key, value] of Object.entries(currentData)) { - const fieldInfo = modelFields[key]; - if (!fieldInfo?.isDataModel) { - continue; - } - - const r = await doApplyMutation( - fieldInfo.type, - value, - mutationModel, - mutationOp, - mutationArgs, - modelMeta, - logging - ); - - if (r && typeof r === 'object') { - resultData = { ...resultData, [key]: r }; - updated = true; - } - } - } - - return updated ? resultData : undefined; -} - -function createMutate(queryModel: string, currentData: any, newData: any, modelMeta: ModelMeta, logging: boolean) { - if (!newData) { - return undefined; - } - - const modelFields = getFields(modelMeta, queryModel); - if (!modelFields) { - return undefined; - } - - const insert: any = {}; - const newDataFields = Object.keys(newData); - - Object.entries(modelFields).forEach(([name, field]) => { - if (field.isDataModel && newData[name]) { - // deal with "connect" - assignForeignKeyFields(field, insert, newData[name]); - return; - } - - if (newDataFields.includes(name)) { - insert[name] = clone(newData[name]); - } else { - const defaultAttr = field.attributes?.find((attr) => attr.name === '@default'); - if (field.type === 'DateTime') { - // default value for DateTime field - if (defaultAttr || field.attributes?.some((attr) => attr.name === '@updatedAt')) { - insert[name] = new Date(); - } - } else if (defaultAttr?.args?.[0]?.value !== undefined) { - // other default value - insert[name] = defaultAttr.args[0].value; - } - } - }); - - // add temp id value - const idFields = getIdFields(modelMeta, queryModel, false); - idFields.forEach((f) => { - if (insert[f.name] === undefined) { - if (f.type === 'Int' || f.type === 'BigInt') { - const currMax = Array.isArray(currentData) - ? Math.max( - ...[...currentData].map((item) => { - const idv = parseInt(item[f.name]); - return isNaN(idv) ? 0 : idv; - }) - ) - : 0; - insert[f.name] = currMax + 1; - } else { - insert[f.name] = uuid(); - } - } - }); - - insert.$optimistic = true; - - if (logging) { - console.log(`Optimistic create for ${queryModel}:`, insert); - } - return [insert, ...(Array.isArray(currentData) ? currentData : [])]; -} - -function updateMutate( - queryModel: string, - currentData: any, - mutateModel: string, - mutateArgs: any, - modelMeta: ModelMeta, - logging: boolean -) { - if (!currentData || typeof currentData !== 'object') { - return undefined; - } - - if (!mutateArgs?.where || typeof mutateArgs.where !== 'object') { - return undefined; - } - - if (!mutateArgs?.data || typeof mutateArgs.data !== 'object') { - return undefined; - } - - if (!idFieldsMatch(mutateModel, currentData, mutateArgs.where, modelMeta)) { - return undefined; - } - - const modelFields = getFields(modelMeta, queryModel); - if (!modelFields) { - return undefined; - } - - let updated = false; - let resultData = currentData; - - for (const [key, value] of Object.entries(mutateArgs.data)) { - const fieldInfo = modelFields[key]; - if (!fieldInfo) { - continue; - } - - if (fieldInfo.isDataModel && !value?.connect) { - // relation field but without "connect" - continue; - } - - if (!updated) { - // clone - resultData = { ...currentData }; - } - - if (fieldInfo.isDataModel) { - // deal with "connect" - assignForeignKeyFields(fieldInfo, resultData, value); - } else { - resultData[key] = clone(value); - } - resultData.$optimistic = true; - updated = true; - - if (logging) { - console.log(`Optimistic update for ${queryModel}:`, resultData); - } - } - - return updated ? resultData : undefined; -} - -function upsertMutate( - queryModel: string, - currentData: any, - model: string, - args: { where: object; create: any; update: any }, - modelMeta: ModelMeta, - logging: boolean -) { - let updated = false; - let resultData = currentData; - - if (Array.isArray(resultData)) { - // check if we should create or update - const foundIndex = resultData.findIndex((x) => idFieldsMatch(model, x, args.where, modelMeta)); - if (foundIndex >= 0) { - const updateResult = updateMutate( - queryModel, - resultData[foundIndex], - model, - { where: args.where, data: args.update }, - modelMeta, - logging - ); - if (updateResult) { - // replace the found item with updated item - resultData = [...resultData.slice(0, foundIndex), updateResult, ...resultData.slice(foundIndex + 1)]; - updated = true; - } - } else { - const createResult = createMutate(queryModel, resultData, args.create, modelMeta, logging); - if (createResult) { - resultData = createResult; - updated = true; - } - } - } else { - // try update only - const updateResult = updateMutate( - queryModel, - resultData, - model, - { where: args.where, data: args.update }, - modelMeta, - logging - ); - if (updateResult) { - resultData = updateResult; - updated = true; - } - } - - return updated ? resultData : undefined; -} - -function deleteMutate( - queryModel: string, - currentData: any, - mutateModel: string, - mutateArgs: any, - modelMeta: ModelMeta, - logging: boolean -) { - // TODO: handle mutation of nested reads? - - if (!currentData || !mutateArgs) { - return undefined; - } - - if (queryModel !== mutateModel) { - return undefined; - } - - let updated = false; - let result = currentData; - - if (Array.isArray(currentData)) { - for (const item of currentData) { - if (idFieldsMatch(mutateModel, item, mutateArgs, modelMeta)) { - result = (result as unknown[]).filter((x) => x !== item); - updated = true; - if (logging) { - console.log(`Optimistic delete for ${queryModel}:`, item); - } - } - } - } else { - if (idFieldsMatch(mutateModel, currentData, mutateArgs, modelMeta)) { - result = null; - updated = true; - if (logging) { - console.log(`Optimistic delete for ${queryModel}:`, currentData); - } - } - } - - return updated ? result : undefined; -} - -function idFieldsMatch(model: string, x: any, y: any, modelMeta: ModelMeta) { - if (!x || !y || typeof x !== 'object' || typeof y !== 'object') { - return false; - } - const idFields = getIdFields(modelMeta, model, false); - if (idFields.length === 0) { - return false; - } - return idFields.every((f) => x[f.name] === y[f.name]); -} - -function assignForeignKeyFields(field: FieldInfo, resultData: any, mutationData: any) { - // convert "connect" like `{ connect: { id: '...' } }` to foreign key fields - // assignment: `{ userId: '...' }` - if (!mutationData?.connect) { - return; - } - - if (!field.foreignKeyMapping) { - return; - } - - for (const [idField, fkField] of Object.entries(field.foreignKeyMapping)) { - if (idField in mutationData.connect) { - resultData[fkField] = mutationData.connect[idField]; - } - } -} diff --git a/packages/runtime/src/cross/nested-read-visitor.ts b/packages/runtime/src/cross/nested-read-visitor.ts deleted file mode 100644 index 80d9a752b..000000000 --- a/packages/runtime/src/cross/nested-read-visitor.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { resolveField, type FieldInfo, type ModelMeta } from './model-meta'; - -export type NestedReadVisitorCallback = { - field?: ( - model: string, - field: FieldInfo | undefined, - kind: 'include' | 'select' | undefined, - args: unknown - ) => void | boolean; -}; - -/** - * Visitor for nested read payload. - */ -export class NestedReadVisitor { - constructor(private readonly modelMeta: ModelMeta, private readonly callback: NestedReadVisitorCallback) {} - - doVisit(model: string, field: FieldInfo | undefined, kind: 'include' | 'select' | undefined, args: unknown) { - if (this.callback.field) { - const r = this.callback.field(model, field, kind, args); - if (r === false) { - return; - } - } - - if (!args || typeof args !== 'object') { - return; - } - - let selectInclude: any; - let nextKind: 'select' | 'include' | undefined; - if ((args as any).select) { - selectInclude = (args as any).select; - nextKind = 'select'; - } else if ((args as any).include) { - selectInclude = (args as any).include; - nextKind = 'include'; - } - - if (selectInclude && typeof selectInclude === 'object') { - for (const [k, v] of Object.entries(selectInclude)) { - if (k === '_count' && typeof v === 'object' && v) { - // recurse into { _count: { ... } } - this.doVisit(model, field, kind, v); - } else { - const field = resolveField(this.modelMeta, model, k); - if (field) { - this.doVisit(field.type, field, nextKind, v); - } - } - } - } - } - - visit(model: string, args: unknown) { - this.doVisit(model, undefined, undefined, args); - } -} diff --git a/packages/runtime/src/cross/nested-write-visitor.ts b/packages/runtime/src/cross/nested-write-visitor.ts deleted file mode 100644 index cff7f8143..000000000 --- a/packages/runtime/src/cross/nested-write-visitor.ts +++ /dev/null @@ -1,357 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import type { FieldInfo, ModelMeta } from './model-meta'; -import { resolveField } from './model-meta'; -import { MaybePromise, PrismaWriteActionType, PrismaWriteActions } from './types'; -import { enumerate, getModelFields } from './utils'; - -type NestingPathItem = { field?: FieldInfo; model: string; where: any; unique: boolean }; - -/** - * Context for visiting - */ -export type NestedWriteVisitorContext = { - /** - * Parent data, can be used to replace fields - */ - parent: any; - - /** - * Current field, undefined if toplevel - */ - field?: FieldInfo; - - /** - * A top-down path of all nested update conditions and corresponding field till now - */ - nestingPath: NestingPathItem[]; -}; - -/** - * NestedWriteVisitor's callback actions. A call back function should return true or void to indicate - * that the visitor should continue traversing its children, or false to stop. It can also return an object - * to let the visitor traverse it instead of its original children. - */ -export type NestedWriterVisitorCallback = { - create?: (model: string, data: any, context: NestedWriteVisitorContext) => MaybePromise; - - createMany?: ( - model: string, - args: { data: any; skipDuplicates?: boolean }, - context: NestedWriteVisitorContext - ) => MaybePromise; - - connectOrCreate?: ( - model: string, - args: { where: object; create: any }, - context: NestedWriteVisitorContext - ) => MaybePromise; - - connect?: ( - model: string, - args: object, - context: NestedWriteVisitorContext - ) => MaybePromise; - - disconnect?: ( - model: string, - args: object, - context: NestedWriteVisitorContext - ) => MaybePromise; - - set?: (model: string, args: object, context: NestedWriteVisitorContext) => MaybePromise; - - update?: (model: string, args: object, context: NestedWriteVisitorContext) => MaybePromise; - - updateMany?: ( - model: string, - args: { where?: object; data: any }, - context: NestedWriteVisitorContext - ) => MaybePromise; - - upsert?: ( - model: string, - args: { where: object; create: any; update: any }, - context: NestedWriteVisitorContext - ) => MaybePromise; - - delete?: ( - model: string, - args: object | boolean, - context: NestedWriteVisitorContext - ) => MaybePromise; - - deleteMany?: ( - model: string, - args: any | object, - context: NestedWriteVisitorContext - ) => MaybePromise; - - field?: ( - field: FieldInfo, - action: PrismaWriteActionType, - data: any, - context: NestedWriteVisitorContext - ) => MaybePromise; -}; - -/** - * Recursive visitor for nested write (create/update) payload. - */ -export class NestedWriteVisitor { - constructor(private readonly modelMeta: ModelMeta, private readonly callback: NestedWriterVisitorCallback) {} - - private isPrismaWriteAction(value: string): value is PrismaWriteActionType { - return PrismaWriteActions.includes(value as PrismaWriteActionType); - } - - /** - * Start visiting - * - * @see NestedWriterVisitorCallback - */ - async visit(model: string, action: PrismaWriteActionType, args: any): Promise { - if (!args) { - return; - } - - let topData = args; - - switch (action) { - // create has its data wrapped in 'data' field - case 'create': - topData = topData.data; - break; - - case 'delete': - case 'deleteMany': - topData = topData.where; - break; - } - - await this.doVisit(model, action, topData, undefined, undefined, []); - } - - private async doVisit( - model: string, - action: PrismaWriteActionType, - data: any, - parent: any, - field: FieldInfo | undefined, - nestingPath: NestingPathItem[] - ): Promise { - if (!data) { - return; - } - - const toplevel = field == undefined; - - const context = { parent, field, nestingPath: [...nestingPath] }; - const pushNewContext = (field: FieldInfo | undefined, model: string, where: any, unique = false) => { - return { ...context, nestingPath: [...context.nestingPath, { field, model, where, unique }] }; - }; - - // visit payload - switch (action) { - case 'create': - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, {}); - let callbackResult: any; - if (this.callback.create) { - callbackResult = await this.callback.create(model, item, newContext); - } - if (callbackResult !== false) { - const subPayload = typeof callbackResult === 'object' ? callbackResult : item; - await this.visitSubPayload(model, action, subPayload, newContext.nestingPath); - } - } - break; - - case 'createMany': - case 'createManyAndReturn': - if (data) { - const newContext = pushNewContext(field, model, {}); - let callbackResult: any; - if (this.callback.createMany) { - callbackResult = await this.callback.createMany(model, data, newContext); - } - if (callbackResult !== false) { - const subPayload = typeof callbackResult === 'object' ? callbackResult : data.data; - await this.visitSubPayload(model, action, subPayload, newContext.nestingPath); - } - } - break; - - case 'connectOrCreate': - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, item.where); - let callbackResult: any; - if (this.callback.connectOrCreate) { - callbackResult = await this.callback.connectOrCreate(model, item, newContext); - } - if (callbackResult !== false) { - const subPayload = typeof callbackResult === 'object' ? callbackResult : item.create; - await this.visitSubPayload(model, action, subPayload, newContext.nestingPath); - } - } - break; - - case 'connect': - if (this.callback.connect) { - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, item, true); - await this.callback.connect(model, item, newContext); - } - } - break; - - case 'disconnect': - // disconnect has two forms: - // if relation is to-many, the payload is a unique filter object - // if relation is to-one, the payload can only be boolean `true` - if (this.callback.disconnect) { - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, item, typeof item === 'object'); - await this.callback.disconnect(model, item, newContext); - } - } - break; - - case 'set': - if (this.callback.set) { - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, item, true); - await this.callback.set(model, item, newContext); - } - } - break; - - case 'update': - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, item.where); - let callbackResult: any; - if (this.callback.update) { - callbackResult = await this.callback.update(model, item, newContext); - } - if (callbackResult !== false) { - const subPayload = - typeof callbackResult === 'object' - ? callbackResult - : typeof item.data === 'object' - ? item.data - : item; - await this.visitSubPayload(model, action, subPayload, newContext.nestingPath); - } - } - break; - - case 'updateMany': - case 'updateManyAndReturn': - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, item.where); - let callbackResult: any; - if (this.callback.updateMany) { - callbackResult = await this.callback.updateMany(model, item, newContext); - } - if (callbackResult !== false) { - const subPayload = typeof callbackResult === 'object' ? callbackResult : item; - await this.visitSubPayload(model, action, subPayload, newContext.nestingPath); - } - } - break; - - case 'upsert': { - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, item.where); - let callbackResult: any; - if (this.callback.upsert) { - callbackResult = await this.callback.upsert(model, item, newContext); - } - if (callbackResult !== false) { - if (typeof callbackResult === 'object') { - await this.visitSubPayload(model, action, callbackResult, newContext.nestingPath); - } else { - await this.visitSubPayload(model, action, item.create, newContext.nestingPath); - await this.visitSubPayload(model, action, item.update, newContext.nestingPath); - } - } - } - break; - } - - case 'delete': { - if (this.callback.delete) { - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, toplevel ? item.where : item); - await this.callback.delete(model, item, newContext); - } - } - break; - } - - case 'deleteMany': - if (this.callback.deleteMany) { - for (const item of this.enumerateReverse(data)) { - const newContext = pushNewContext(field, model, toplevel ? item.where : item); - await this.callback.deleteMany(model, item, newContext); - } - } - break; - - default: { - throw new Error(`unhandled action type ${action}`); - } - } - } - - private async visitSubPayload( - model: string, - action: PrismaWriteActionType, - payload: any, - nestingPath: NestingPathItem[] - ) { - for (const item of enumerate(payload)) { - for (const field of getModelFields(item)) { - const fieldInfo = resolveField(this.modelMeta, model, field); - if (!fieldInfo) { - continue; - } - - if (fieldInfo.isDataModel) { - if (item[field]) { - // recurse into nested payloads - for (const [subAction, subData] of Object.entries(item[field])) { - if (this.isPrismaWriteAction(subAction) && subData) { - await this.doVisit(fieldInfo.type, subAction, subData, item[field], fieldInfo, [ - ...nestingPath, - ]); - } - } - } - } else { - // visit plain field - if (this.callback.field) { - await this.callback.field(fieldInfo, action, item[field], { - parent: item, - nestingPath, - field: fieldInfo, - }); - } - } - } - } - } - - // enumerate a (possible) array in reverse order, so that the enumeration - // callback can safely delete the current item - private *enumerateReverse(data: any) { - if (Array.isArray(data)) { - for (let i = data.length - 1; i >= 0; i--) { - yield data[i]; - } - } else { - yield data; - } - } -} diff --git a/packages/runtime/src/cross/query-analyzer.ts b/packages/runtime/src/cross/query-analyzer.ts deleted file mode 100644 index ad8949998..000000000 --- a/packages/runtime/src/cross/query-analyzer.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { lowerCaseFirst } from '../local-helpers'; -import type { ModelMeta } from './model-meta'; -import { NestedReadVisitor } from './nested-read-visitor'; -import { NestedWriteVisitor } from './nested-write-visitor'; -import type { PrismaWriteActionType } from './types'; -import { getModelInfo } from './utils'; - -/** - * Gets models read (including nested ones) given a query args. - * @param model - * @param targetModels - * @param modelMeta - * @param args - * @returns - */ -export function getReadModels(model: string, modelMeta: ModelMeta, args: any) { - const result = new Set(); - result.add(model); - const visitor = new NestedReadVisitor(modelMeta, { - field: (model) => { - result.add(model); - return true; - }, - }); - visitor.visit(model, args); - return [...result]; -} - -/** - * Gets mutated models (including nested ones) given a mutation args. - */ -export async function getMutatedModels( - model: string, - operation: PrismaWriteActionType, - mutationArgs: any, - modelMeta: ModelMeta -) { - const result = new Set(); - result.add(model); - - if (mutationArgs) { - const addModel = (model: string) => void result.add(model); - - // add models that are cascaded deleted recursively - const addCascades = (model: string) => { - const cascades = new Set(); - const visited = new Set(); - collectDeleteCascades(model, modelMeta, cascades, visited); - cascades.forEach((m) => addModel(m)); - }; - - const visitor = new NestedWriteVisitor(modelMeta, { - create: addModel, - createMany: addModel, - connectOrCreate: addModel, - connect: addModel, - disconnect: addModel, - set: addModel, - update: addModel, - updateMany: addModel, - upsert: addModel, - delete: (model) => { - addModel(model); - addCascades(model); - }, - deleteMany: (model) => { - addModel(model); - addCascades(model); - }, - }); - await visitor.visit(model, operation, mutationArgs); - } - - // include delegate base models recursively - result.forEach((m) => { - getBaseRecursively(m, modelMeta, result); - }); - - return [...result]; -} - -function collectDeleteCascades(model: string, modelMeta: ModelMeta, result: Set, visited: Set) { - if (visited.has(model)) { - // break circle - return; - } - visited.add(model); - - const cascades = modelMeta.deleteCascade?.[lowerCaseFirst(model)]; - - if (!cascades) { - return; - } - - cascades.forEach((m) => { - result.add(m); - collectDeleteCascades(m, modelMeta, result, visited); - }); -} - -function getBaseRecursively(model: string, modelMeta: ModelMeta, result: Set) { - const bases = getModelInfo(modelMeta, model)?.baseTypes; - if (bases) { - bases.forEach((base) => { - result.add(base); - getBaseRecursively(base, modelMeta, result); - }); - } -} diff --git a/packages/runtime/src/cross/types.ts b/packages/runtime/src/cross/types.ts deleted file mode 100644 index 50d4f1e02..000000000 --- a/packages/runtime/src/cross/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Prisma write operation kinds - */ -export const PrismaWriteActions = [ - 'create', - 'createMany', - 'createManyAndReturn', - 'connectOrCreate', - 'update', - 'updateMany', - 'updateManyAndReturn', - 'upsert', - 'connect', - 'disconnect', - 'set', - 'delete', - 'deleteMany', -] as const; - -/** - * Prisma write operation kinds - */ -export type PrismaWriteActionType = (typeof PrismaWriteActions)[number]; - -/** - * Maybe promise - */ -export type MaybePromise = T | Promise | PromiseLike; diff --git a/packages/runtime/src/cross/utils.ts b/packages/runtime/src/cross/utils.ts deleted file mode 100644 index c56ff3cf9..000000000 --- a/packages/runtime/src/cross/utils.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { lowerCaseFirst } from '../local-helpers'; -import { requireField, type ModelInfo, type ModelMeta, type TypeDefInfo } from '.'; - -/** - * Gets field names in a data model entity, filtering out internal fields. - */ -export function getModelFields(data: object) { - return data ? Object.keys(data) : []; -} - -/** - * Array or scalar - */ -export type Enumerable = T | Array; - -/** - * Uniformly enumerates an array or scalar. - */ -export function enumerate(x: Enumerable) { - if (x === null || x === undefined) { - return []; - } else if (Array.isArray(x)) { - return x; - } else { - return [x]; - } -} - -/** - * Zip two arrays or scalars. - */ -export function zip(x: Enumerable, y: Enumerable): Array<[T1, T2]> { - if (Array.isArray(x)) { - if (!Array.isArray(y)) { - throw new Error('x and y should be both array or both scalar'); - } - if (x.length !== y.length) { - throw new Error('x and y should have the same length'); - } - return x.map((_, i) => [x[i], y[i]] as [T1, T2]); - } else { - if (Array.isArray(y)) { - throw new Error('x and y should be both array or both scalar'); - } - return [[x, y]]; - } -} - -/** - * Gets ID fields of a model. - */ -export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound = false) { - const uniqueConstraints = modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints ?? {}; - - const entries = Object.values(uniqueConstraints); - if (entries.length === 0) { - if (throwIfNotFound) { - throw new Error(`Model ${model} does not have any id field`); - } - return []; - } - - return entries[0].fields.map((f) => requireField(modelMeta, model, f)); -} - -/** - * Gets info for a model. - */ -export function getModelInfo( - modelMeta: ModelMeta, - model: string, - throwIfNotFound: Throw = false as Throw -): Throw extends true ? ModelInfo : ModelInfo | undefined { - const info = modelMeta.models[lowerCaseFirst(model)]; - if (!info && throwIfNotFound) { - throw new Error(`Unable to load info for ${model}`); - } - return info; -} - -/** - * Gets info for a type def. - */ -export function getTypeDefInfo( - modelMeta: ModelMeta, - typeDef: string, - throwIfNotFound: Throw = false as Throw -): Throw extends true ? TypeDefInfo : TypeDefInfo | undefined { - const info = modelMeta.typeDefs?.[lowerCaseFirst(typeDef)]; - if (!info && throwIfNotFound) { - throw new Error(`Unable to load info for ${typeDef}`); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return info as any; -} - -/** - * Checks if a model is a delegate model. - */ -export function isDelegateModel(modelMeta: ModelMeta, model: string) { - return !!getModelInfo(modelMeta, model)?.attributes?.some((attr) => attr.name === '@@delegate'); -} diff --git a/packages/runtime/src/edge.ts b/packages/runtime/src/edge.ts deleted file mode 100644 index cce09ec57..000000000 --- a/packages/runtime/src/edge.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './enhance-edge'; diff --git a/packages/runtime/src/encryption/index.ts b/packages/runtime/src/encryption/index.ts deleted file mode 100644 index d4cb31db6..000000000 --- a/packages/runtime/src/encryption/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { _decrypt, _encrypt, ENCRYPTION_KEY_BYTES, getKeyDigest, loadKey } from './utils'; - -/** - * Default encrypter - */ -export class Encrypter { - private key: CryptoKey | undefined; - private keyDigest: string | undefined; - - constructor(private readonly encryptionKey: Uint8Array) { - if (encryptionKey.length !== ENCRYPTION_KEY_BYTES) { - throw new Error(`Encryption key must be ${ENCRYPTION_KEY_BYTES} bytes`); - } - } - - /** - * Encrypts the given data - */ - async encrypt(data: string): Promise { - if (!this.key) { - this.key = await loadKey(this.encryptionKey, ['encrypt']); - } - - if (!this.keyDigest) { - this.keyDigest = await getKeyDigest(this.encryptionKey); - } - - return _encrypt(data, this.key, this.keyDigest); - } -} - -/** - * Default decrypter - */ -export class Decrypter { - private keys: Array<{ key: CryptoKey; digest: string }> = []; - - constructor(private readonly decryptionKeys: Uint8Array[]) { - if (decryptionKeys.length === 0) { - throw new Error('At least one decryption key must be provided'); - } - - for (const key of decryptionKeys) { - if (key.length !== ENCRYPTION_KEY_BYTES) { - throw new Error(`Decryption key must be ${ENCRYPTION_KEY_BYTES} bytes`); - } - } - } - - /** - * Decrypts the given data - */ - async decrypt(data: string): Promise { - if (this.keys.length === 0) { - this.keys = await Promise.all( - this.decryptionKeys.map(async (key) => ({ - key: await loadKey(key, ['decrypt']), - digest: await getKeyDigest(key), - })) - ); - } - - return _decrypt(data, async (digest) => - this.keys.filter((entry) => entry.digest === digest).map((entry) => entry.key) - ); - } -} diff --git a/packages/runtime/src/encryption/utils.ts b/packages/runtime/src/encryption/utils.ts deleted file mode 100644 index 51ab41dc7..000000000 --- a/packages/runtime/src/encryption/utils.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { z } from 'zod'; - -export const ENCRYPTER_VERSION = 1; -export const ENCRYPTION_KEY_BYTES = 32; -export const IV_BYTES = 12; -export const ALGORITHM = 'AES-GCM'; -export const KEY_DIGEST_BYTES = 8; - -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); - -const encryptionMetaSchema = z.object({ - // version - v: z.number(), - // algorithm - a: z.string(), - // key digest - k: z.string(), -}); - -export async function loadKey(key: Uint8Array, keyUsages: KeyUsage[]): Promise { - return crypto.subtle.importKey('raw', key, ALGORITHM, false, keyUsages); -} - -export async function getKeyDigest(key: Uint8Array) { - const rawDigest = await crypto.subtle.digest('SHA-256', key); - return new Uint8Array(rawDigest.slice(0, KEY_DIGEST_BYTES)).reduce( - (acc, byte) => acc + byte.toString(16).padStart(2, '0'), - '' - ); -} - -export async function _encrypt(data: string, key: CryptoKey, keyDigest: string): Promise { - const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES)); - const encrypted = await crypto.subtle.encrypt( - { - name: ALGORITHM, - iv, - }, - key, - encoder.encode(data) - ); - - // combine IV and encrypted data into a single array of bytes - const cipherBytes = [...iv, ...new Uint8Array(encrypted)]; - - // encryption metadata - const meta = { v: ENCRYPTER_VERSION, a: ALGORITHM, k: keyDigest }; - - // convert concatenated result to base64 string - return `${btoa(JSON.stringify(meta))}.${btoa(String.fromCharCode(...cipherBytes))}`; -} - -export async function _decrypt(data: string, findKey: (digest: string) => Promise): Promise { - const [metaText, cipherText] = data.split('.'); - if (!metaText || !cipherText) { - throw new Error('Malformed encrypted data'); - } - - let metaObj: unknown; - try { - metaObj = JSON.parse(atob(metaText)); - } catch (error) { - throw new Error('Malformed metadata'); - } - - // parse meta - const { a: algorithm, k: keyDigest } = encryptionMetaSchema.parse(metaObj); - - // find a matching decryption key - const keys = await findKey(keyDigest); - if (keys.length === 0) { - throw new Error('No matching decryption key found'); - } - - // convert base64 back to bytes - const bytes = Uint8Array.from(atob(cipherText), (c) => c.charCodeAt(0)); - - // extract IV from the head - const iv = bytes.slice(0, IV_BYTES); - const cipher = bytes.slice(IV_BYTES); - let lastError: unknown; - - for (const key of keys) { - let decrypted: ArrayBuffer; - try { - decrypted = await crypto.subtle.decrypt({ name: algorithm, iv }, key, cipher); - } catch (err) { - lastError = err; - continue; - } - return decoder.decode(decrypted); - } - - throw lastError; -} diff --git a/packages/runtime/src/enhance-edge.d.ts b/packages/runtime/src/enhance-edge.d.ts deleted file mode 100644 index 2f6d6218e..000000000 --- a/packages/runtime/src/enhance-edge.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @ts-expect-error stub for re-exporting generated code -export { auth, enhance } from '.zenstack/enhance-edge'; diff --git a/packages/runtime/src/enhance.d.ts b/packages/runtime/src/enhance.d.ts deleted file mode 100644 index 9a4fe97cd..000000000 --- a/packages/runtime/src/enhance.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @ts-expect-error stub for re-exporting generated code -export { auth, enhance, type PrismaClient, type Enhanced } from '.zenstack/enhance'; diff --git a/packages/runtime/src/enhancements/edge/create-enhancement.ts b/packages/runtime/src/enhancements/edge/create-enhancement.ts deleted file mode 120000 index e06076ec0..000000000 --- a/packages/runtime/src/enhancements/edge/create-enhancement.ts +++ /dev/null @@ -1 +0,0 @@ -../node/create-enhancement.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/default-auth.ts b/packages/runtime/src/enhancements/edge/default-auth.ts deleted file mode 120000 index 63249aec9..000000000 --- a/packages/runtime/src/enhancements/edge/default-auth.ts +++ /dev/null @@ -1 +0,0 @@ -../node/default-auth.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/delegate.ts b/packages/runtime/src/enhancements/edge/delegate.ts deleted file mode 120000 index fc4213954..000000000 --- a/packages/runtime/src/enhancements/edge/delegate.ts +++ /dev/null @@ -1 +0,0 @@ -../node/delegate.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/encryption.ts b/packages/runtime/src/enhancements/edge/encryption.ts deleted file mode 120000 index 9931fc8ea..000000000 --- a/packages/runtime/src/enhancements/edge/encryption.ts +++ /dev/null @@ -1 +0,0 @@ -../node/encryption.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/index.ts b/packages/runtime/src/enhancements/edge/index.ts deleted file mode 120000 index 1baad1b89..000000000 --- a/packages/runtime/src/enhancements/edge/index.ts +++ /dev/null @@ -1 +0,0 @@ -../node/index.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/json-processor.ts b/packages/runtime/src/enhancements/edge/json-processor.ts deleted file mode 120000 index 4144fc6f4..000000000 --- a/packages/runtime/src/enhancements/edge/json-processor.ts +++ /dev/null @@ -1 +0,0 @@ -../node/json-processor.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/logger.ts b/packages/runtime/src/enhancements/edge/logger.ts deleted file mode 120000 index b223d7301..000000000 --- a/packages/runtime/src/enhancements/edge/logger.ts +++ /dev/null @@ -1 +0,0 @@ -../node/logger.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/omit.ts b/packages/runtime/src/enhancements/edge/omit.ts deleted file mode 120000 index eb506bb87..000000000 --- a/packages/runtime/src/enhancements/edge/omit.ts +++ /dev/null @@ -1 +0,0 @@ -../node/omit.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/password.ts b/packages/runtime/src/enhancements/edge/password.ts deleted file mode 120000 index 8d06b6e3c..000000000 --- a/packages/runtime/src/enhancements/edge/password.ts +++ /dev/null @@ -1 +0,0 @@ -../node/password.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/policy/check-utils.ts b/packages/runtime/src/enhancements/edge/policy/check-utils.ts deleted file mode 100644 index b3f27c4fb..000000000 --- a/packages/runtime/src/enhancements/edge/policy/check-utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ModelMeta } from '..'; -import type { DbClientContract } from '../../../types'; -import { PermissionCheckArgs } from '../types'; -import { PolicyUtil } from './policy-utils'; - -export async function checkPermission( - _model: string, - _args: PermissionCheckArgs, - _modelMeta: ModelMeta, - _policyUtils: PolicyUtil, - _prisma: DbClientContract, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _prismaModule: any -): Promise { - throw new Error('`check()` API is not supported on edge runtime'); -} diff --git a/packages/runtime/src/enhancements/edge/policy/handler.ts b/packages/runtime/src/enhancements/edge/policy/handler.ts deleted file mode 120000 index 1439205ac..000000000 --- a/packages/runtime/src/enhancements/edge/policy/handler.ts +++ /dev/null @@ -1 +0,0 @@ -../../node/policy/handler.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/policy/index.ts b/packages/runtime/src/enhancements/edge/policy/index.ts deleted file mode 120000 index 3d4a1fa9c..000000000 --- a/packages/runtime/src/enhancements/edge/policy/index.ts +++ /dev/null @@ -1 +0,0 @@ -../../node/policy/index.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/policy/policy-utils.ts b/packages/runtime/src/enhancements/edge/policy/policy-utils.ts deleted file mode 120000 index 7dadd4769..000000000 --- a/packages/runtime/src/enhancements/edge/policy/policy-utils.ts +++ /dev/null @@ -1 +0,0 @@ -../../node/policy/policy-utils.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/promise.ts b/packages/runtime/src/enhancements/edge/promise.ts deleted file mode 120000 index a00819e60..000000000 --- a/packages/runtime/src/enhancements/edge/promise.ts +++ /dev/null @@ -1 +0,0 @@ -../node/promise.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/proxy.ts b/packages/runtime/src/enhancements/edge/proxy.ts deleted file mode 120000 index de440c0e0..000000000 --- a/packages/runtime/src/enhancements/edge/proxy.ts +++ /dev/null @@ -1 +0,0 @@ -../node/proxy.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/query-utils.ts b/packages/runtime/src/enhancements/edge/query-utils.ts deleted file mode 120000 index 4a42067b1..000000000 --- a/packages/runtime/src/enhancements/edge/query-utils.ts +++ /dev/null @@ -1 +0,0 @@ -../node/query-utils.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/types.ts b/packages/runtime/src/enhancements/edge/types.ts deleted file mode 120000 index 3faa30754..000000000 --- a/packages/runtime/src/enhancements/edge/types.ts +++ /dev/null @@ -1 +0,0 @@ -../node/types.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/utils.ts b/packages/runtime/src/enhancements/edge/utils.ts deleted file mode 120000 index c6f4d9b46..000000000 --- a/packages/runtime/src/enhancements/edge/utils.ts +++ /dev/null @@ -1 +0,0 @@ -../node/utils.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/edge/where-visitor.ts b/packages/runtime/src/enhancements/edge/where-visitor.ts deleted file mode 120000 index 7b6cb94d8..000000000 --- a/packages/runtime/src/enhancements/edge/where-visitor.ts +++ /dev/null @@ -1 +0,0 @@ -../node/where-visitor.ts \ No newline at end of file diff --git a/packages/runtime/src/enhancements/node/create-enhancement.ts b/packages/runtime/src/enhancements/node/create-enhancement.ts deleted file mode 100644 index 07f905182..000000000 --- a/packages/runtime/src/enhancements/node/create-enhancement.ts +++ /dev/null @@ -1,170 +0,0 @@ -import semver from 'semver'; -import { PRISMA_MINIMUM_VERSION } from '../../constants'; -import { isDelegateModel, type ModelMeta } from '../../cross'; -import type { - DbClientContract, - EnhancementContext, - EnhancementKind, - EnhancementOptions, - ZodSchemas, -} from '../../types'; -import { withDefaultAuth } from './default-auth'; -import { withDelegate } from './delegate'; -import { withEncrypted } from './encryption'; -import { withJsonProcessor } from './json-processor'; -import { Logger } from './logger'; -import { withOmit } from './omit'; -import { withPassword } from './password'; -import { policyProcessIncludeRelationPayload, withPolicy } from './policy'; -import type { PolicyDef } from './types'; - -/** - * All enhancement kinds - */ -const ALL_ENHANCEMENTS: EnhancementKind[] = ['password', 'omit', 'policy', 'validation', 'delegate', 'encryption']; - -/** - * Options for {@link createEnhancement} - * - * @private - */ -export type InternalEnhancementOptions = EnhancementOptions & { - /** - * Policy definition - */ - policy: PolicyDef; - - /** - * Model metadata - */ - modelMeta: ModelMeta; - - /** - * Zod schemas for validation - */ - zodSchemas?: ZodSchemas; - - /** - * The Node module that contains PrismaClient - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - prismaModule: any; - - /** - * A callback shared among enhancements to process the payload for including a relation - * field. e.g.: `{ author: true }`. - */ - processIncludeRelationPayload?: ( - prisma: DbClientContract, - model: string, - payload: unknown, - options: InternalEnhancementOptions, - context: EnhancementContext | undefined - ) => Promise; -}; - -/** - * Gets a Prisma client enhanced with all enhancement behaviors, including access - * policy, field validation, field omission and password hashing. - * - * @private - * - * @param prisma The Prisma client to enhance. - * @param context Context. - * @param options Options. - */ -export function createEnhancement( - prisma: DbClient, - options: InternalEnhancementOptions, - context?: EnhancementContext -) { - if (!prisma) { - throw new Error('Invalid prisma instance'); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const prismaVer = (prisma as any)._clientVersion; - if (prismaVer && semver.lt(prismaVer, PRISMA_MINIMUM_VERSION)) { - console.warn( - `ZenStack requires Prisma version "${PRISMA_MINIMUM_VERSION}" or higher. Detected version is "${prismaVer}".` - ); - } - - // TODO: move the detection logic into each enhancement - // TODO: how to properly cache the detection result? - - const allFields = Object.values(options.modelMeta.models).flatMap((modelInfo) => Object.values(modelInfo.fields)); - if (options.modelMeta.typeDefs) { - allFields.push( - ...Object.values(options.modelMeta.typeDefs).flatMap((typeDefInfo) => Object.values(typeDefInfo.fields)) - ); - } - - const hasPassword = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@password')); - const hasEncrypted = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@encrypted')); - const hasOmit = allFields.some((field) => field.attributes?.some((attr) => attr.name === '@omit')); - const hasDefaultAuth = allFields.some((field) => field.defaultValueProvider); - const hasTypeDefField = allFields.some((field) => field.isTypeDef); - - const kinds = options.kinds ?? ALL_ENHANCEMENTS; - let result = prisma; - - // delegate proxy needs to be wrapped inside policy proxy, since it may translate `deleteMany` - // and `updateMany` to plain `delete` and `update` - if (Object.values(options.modelMeta.models).some((model) => isDelegateModel(options.modelMeta, model.name))) { - if (!kinds.includes('delegate')) { - const logger = new Logger(prisma); - logger.warn( - 'Your ZModel contains delegate models but "delegate" enhancement kind is not enabled. This may result in unexpected behavior.' - ); - } else { - result = withDelegate(result, options, context); - } - } - - // password and encrypted enhancement must be applied prior to policy because it changes then length of the field - // and can break validation rules like `@length` - if (hasPassword && kinds.includes('password')) { - // @password proxy - result = withPassword(result, options); - } - - if (hasEncrypted && kinds.includes('encryption')) { - if (!options.encryption) { - throw new Error('Encryption options are required for @encrypted enhancement'); - } - - // @encrypted proxy - result = withEncrypted(result, options); - } - - // 'policy' and 'validation' enhancements are both enabled by `withPolicy` - if (kinds.includes('policy') || kinds.includes('validation')) { - result = withPolicy(result, options, context); - - // if any enhancement is to introduce an inclusion of a relation field, the - // inclusion payload must be processed by the policy enhancement for injecting - // access control rules - - // TODO: this is currently a global callback shared among all enhancements, which - // is far from ideal - - options.processIncludeRelationPayload = policyProcessIncludeRelationPayload; - - if (kinds.includes('policy') && hasDefaultAuth) { - // @default(auth()) proxy - result = withDefaultAuth(result, options, context); - } - } - - if (hasOmit && kinds.includes('omit')) { - // @omit proxy - result = withOmit(result, options); - } - - if (hasTypeDefField) { - result = withJsonProcessor(result, options); - } - - return result; -} diff --git a/packages/runtime/src/enhancements/node/default-auth.ts b/packages/runtime/src/enhancements/node/default-auth.ts deleted file mode 100644 index 8bdd76490..000000000 --- a/packages/runtime/src/enhancements/node/default-auth.ts +++ /dev/null @@ -1,232 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { ACTIONS_WITH_WRITE_PAYLOAD } from '../../constants'; -import { - FieldInfo, - NestedWriteVisitor, - NestedWriteVisitorContext, - PrismaWriteActionType, - clone, - enumerate, - getFields, - getModelInfo, - getTypeDefInfo, - requireField, -} from '../../cross'; -import { DbClientContract, EnhancementContext } from '../../types'; -import { InternalEnhancementOptions } from './create-enhancement'; -import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; -import { isUnsafeMutate, prismaClientValidationError } from './utils'; - -/** - * Gets an enhanced Prisma client that supports `@default(auth())` attribute. - * - * @private - */ -export function withDefaultAuth( - prisma: DbClient, - options: InternalEnhancementOptions, - context: EnhancementContext = {} -): DbClient { - return makeProxy( - prisma, - options.modelMeta, - (_prisma, model) => new DefaultAuthHandler(_prisma as DbClientContract, model, options, context), - 'defaultAuth' - ); -} - -class DefaultAuthHandler extends DefaultPrismaProxyHandler { - private readonly userContext: any; - - constructor( - prisma: DbClientContract, - model: string, - options: InternalEnhancementOptions, - private readonly context: EnhancementContext - ) { - super(prisma, model, options); - - this.userContext = this.context.user; - } - - // base override - protected async preprocessArgs(action: PrismaProxyActions, args: any) { - if (args && ACTIONS_WITH_WRITE_PAYLOAD.includes(action)) { - const newArgs = await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args); - return newArgs; - } - return args; - } - - private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) { - const newArgs = clone(args); - - const processCreatePayload = (model: string, data: any, context: NestedWriteVisitorContext) => { - const fields = getFields(this.options.modelMeta, model); - for (const fieldInfo of Object.values(fields)) { - if (fieldInfo.isTypeDef) { - this.setDefaultValueForTypeDefData(fieldInfo.type, data[fieldInfo.name]); - continue; - } - - if (fieldInfo.name in data) { - // create payload already sets field value - continue; - } - - if (!fieldInfo.defaultValueProvider) { - // field doesn't have a runtime default value provider - continue; - } - - const defaultValue = this.getDefaultValue(fieldInfo); - if (defaultValue !== undefined) { - // set field value extracted from `auth()` - this.setDefaultValueForModelData(fieldInfo, model, data, defaultValue, context); - } - } - }; - - // visit create payload and set default value to fields using `auth()` in `@default()` - const visitor = new NestedWriteVisitor(this.options.modelMeta, { - create: (model, data, context) => { - processCreatePayload(model, data, context); - }, - - upsert: (model, data, context) => { - processCreatePayload(model, data.create, context); - }, - - createMany: (model, args, context) => { - for (const item of enumerate(args.data)) { - processCreatePayload(model, item, context); - } - }, - }); - - await visitor.visit(model, action, newArgs); - return newArgs; - } - - private setDefaultValueForModelData( - fieldInfo: FieldInfo, - model: string, - data: any, - authDefaultValue: unknown, - context: NestedWriteVisitorContext - ) { - if (fieldInfo.isForeignKey) { - // if the field being inspected is a fk field, there are several cases we should not - // set the default value or should not set directly - - // if the field is a fk, and the relation field is already set, we should not override it - if (fieldInfo.relationField && fieldInfo.relationField in data) { - return; - } - - if (context.field?.backLink) { - // if the fk field is in a creation context where its implied by the parent, - // we should not set the default value, e.g.: - // - // ``` - // parent.create({ data: { child: { create: {} } } }) - // ``` - // - // even if child's fk to parent has a default value, we should not set default - // value here - - // get the opposite side of the relation for the current create context - const oppositeRelationField = requireField(this.options.modelMeta, model, context.field.backLink); - if ( - oppositeRelationField.foreignKeyMapping && - Object.values(oppositeRelationField.foreignKeyMapping).includes(fieldInfo.name) - ) { - return; - } - } - - if (!isUnsafeMutate(model, data, this.options.modelMeta)) { - // if the field is a fk, and the create payload is not unsafe, we need to translate - // the fk field setting to a `connect` of the corresponding relation field - const relFieldName = fieldInfo.relationField; - if (!relFieldName) { - throw new Error( - `Field \`${fieldInfo.name}\` is a foreign key field but no corresponding relation field is found` - ); - } - const relationField = requireField(this.options.modelMeta, model, relFieldName); - - // construct a `{ connect: { ... } }` payload - let connect = data[relationField.name]?.connect; - if (!connect) { - connect = {}; - data[relationField.name] = { connect }; - } - - // sets the opposite fk field to value `authDefaultValue` - const oppositeFkFieldName = this.getOppositeFkFieldName(relationField, fieldInfo); - if (!oppositeFkFieldName) { - throw new Error( - `Cannot find opposite foreign key field for \`${fieldInfo.name}\` in relation field \`${relFieldName}\`` - ); - } - connect[oppositeFkFieldName] = authDefaultValue; - return; - } - } - - // set default value directly - data[fieldInfo.name] = authDefaultValue; - } - - private getOppositeFkFieldName(relationField: FieldInfo, fieldInfo: FieldInfo) { - if (!relationField.foreignKeyMapping) { - return undefined; - } - const entry = Object.entries(relationField.foreignKeyMapping).find(([, v]) => v === fieldInfo.name); - return entry?.[0]; - } - - private getDefaultValue(fieldInfo: FieldInfo) { - if (!this.userContext) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - `Evaluating default value of field \`${fieldInfo.name}\` requires a user context` - ); - } - return fieldInfo.defaultValueProvider?.(this.userContext); - } - - private setDefaultValueForTypeDefData(type: string, data: any) { - if (!data || (typeof data !== 'object' && !Array.isArray(data))) { - return; - } - - const typeDef = getTypeDefInfo(this.options.modelMeta, type); - if (!typeDef) { - return; - } - - enumerate(data).forEach((item) => { - if (!item || typeof item !== 'object') { - return; - } - - for (const fieldInfo of Object.values(typeDef.fields)) { - if (fieldInfo.isTypeDef) { - // recurse - this.setDefaultValueForTypeDefData(fieldInfo.type, item[fieldInfo.name]); - } else if (!(fieldInfo.name in item)) { - // set default value if the payload doesn't set the field - const defaultValue = this.getDefaultValue(fieldInfo); - if (defaultValue !== undefined) { - item[fieldInfo.name] = defaultValue; - } - } - } - }); - } -} diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts deleted file mode 100644 index 977e1c199..000000000 --- a/packages/runtime/src/enhancements/node/delegate.ts +++ /dev/null @@ -1,1595 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import deepmerge, { type ArrayMergeOptions } from 'deepmerge'; -import { DELEGATE_AUX_RELATION_PREFIX } from '../../constants'; -import { - FieldInfo, - ModelInfo, - NestedWriteVisitor, - clone, - enumerate, - getFields, - getIdFields, - getModelInfo, - isDelegateModel, - resolveField, -} from '../../cross'; -import { isPlainObject, simpleTraverse, lowerCaseFirst } from '../../local-helpers'; -import type { CrudContract, DbClientContract, EnhancementContext } from '../../types'; -import type { InternalEnhancementOptions } from './create-enhancement'; -import { Logger } from './logger'; -import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; -import { QueryUtils } from './query-utils'; -import { formatObject, prismaClientValidationError } from './utils'; - -export function withDelegate( - prisma: DbClient, - options: InternalEnhancementOptions, - context: EnhancementContext | undefined -): DbClient { - return makeProxy( - prisma, - options.modelMeta, - (_prisma, model) => new DelegateProxyHandler(_prisma as DbClientContract, model, options, context), - 'delegate' - ); -} - -export class DelegateProxyHandler extends DefaultPrismaProxyHandler { - private readonly logger: Logger; - private readonly queryUtils: QueryUtils; - - constructor( - prisma: DbClientContract, - model: string, - options: InternalEnhancementOptions, - private readonly context: EnhancementContext | undefined - ) { - super(prisma, model, options); - this.logger = new Logger(prisma); - this.queryUtils = new QueryUtils(prisma, this.options); - } - - // #region find - - override findFirst(args: any): Promise { - return this.doFind(this.prisma, this.model, 'findFirst', args); - } - - override findFirstOrThrow(args: any): Promise { - return this.doFind(this.prisma, this.model, 'findFirstOrThrow', args); - } - - override findUnique(args: any): Promise { - return this.doFind(this.prisma, this.model, 'findUnique', args); - } - - override findUniqueOrThrow(args: any): Promise { - return this.doFind(this.prisma, this.model, 'findUniqueOrThrow', args); - } - - override async findMany(args: any): Promise { - return this.doFind(this.prisma, this.model, 'findMany', args); - } - - private async doFind( - db: CrudContract, - model: string, - method: 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany', - args: any - ) { - if (!this.involvesDelegateModel(model)) { - return super[method](args); - } - - args = args ? clone(args) : {}; - - this.injectWhereHierarchy(model, args?.where); - await this.injectSelectIncludeHierarchy(model, args); - - // discriminator field is needed during post process to determine the - // actual concrete model type - this.ensureDiscriminatorSelection(model, args); - - if (args.orderBy) { - // `orderBy` may contain fields from base types - enumerate(args.orderBy).forEach((item) => this.injectWhereHierarchy(model, item)); - } - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`${method}\` ${this.getModelName(model)}: ${formatObject(args)}`); - } - const entity = await db[model][method](args); - - if (Array.isArray(entity)) { - return entity.map((item) => this.assembleHierarchy(model, item)); - } else { - return this.assembleHierarchy(model, entity); - } - } - - private ensureDiscriminatorSelection(model: string, args: any) { - const modelInfo = getModelInfo(this.options.modelMeta, model); - if (!modelInfo?.discriminator) { - return; - } - - if (args.select && typeof args.select === 'object') { - args.select[modelInfo.discriminator] = true; - return; - } - - if (args.omit && typeof args.omit === 'object') { - args.omit[modelInfo.discriminator] = false; - return; - } - } - - private injectWhereHierarchy(model: string, where: any) { - if (!where || !isPlainObject(where)) { - return; - } - - Object.entries(where).forEach(([field, value]) => { - if (['AND', 'OR', 'NOT'].includes(field)) { - // recurse into logical group - enumerate(value).forEach((item) => this.injectWhereHierarchy(model, item)); - return; - } - - const fieldInfo = resolveField(this.options.modelMeta, model, field); - if (!fieldInfo?.inheritedFrom) { - // not an inherited field, inject and continue - if (fieldInfo?.isDataModel) { - this.injectWhereHierarchy(fieldInfo.type, value); - } - return; - } - - let base = this.getBaseModel(model); - let target = where; - - while (base) { - const baseRelationName = this.makeAuxRelationName(base); - - // prepare base layer where - let thisLayer: any; - if (target[baseRelationName]) { - thisLayer = target[baseRelationName]; - } else { - thisLayer = target[baseRelationName] = {}; - } - - if (base.name === fieldInfo.inheritedFrom) { - if (fieldInfo.isDataModel) { - this.injectWhereHierarchy(base.name, value); - } - thisLayer[field] = value; - delete where[field]; - break; - } else { - target = thisLayer; - base = this.getBaseModel(base.name); - } - } - }); - } - - private async injectSelectIncludeHierarchy(model: string, args: any) { - if (!args || typeof args !== 'object') { - return; - } - - // there're two cases where we need to inject polymorphic base hierarchy for fields - // defined in base models - // 1. base fields mentioned in select/include clause - // { select: { fieldFromBase: true } } => { select: { delegate_aux_[Base]: { fieldFromBase: true } } } - // 2. base fields mentioned in _count select/include clause - // { select: { _count: { select: { fieldFromBase: true } } } } => { select: { delegate_aux_[Base]: { select: { _count: { select: { fieldFromBase: true } } } } } } - // - // Note that although structurally similar, we need to correctly deal with different injection location of the "delegate_aux" hierarchy - - // selectors for the above two cases - const selectors = [ - // regular select: { select: { field: true } } - (payload: any) => ({ data: payload.select, kind: 'select' as const, isCount: false }), - // regular include: { include: { field: true } } - (payload: any) => ({ data: payload.include, kind: 'include' as const, isCount: false }), - // select _count: { select: { _count: { select: { field: true } } } } - (payload: any) => ({ - data: payload.select?._count?.select, - kind: 'select' as const, - isCount: true, - }), - // include _count: { include: { _count: { select: { field: true } } } } - (payload: any) => ({ - data: payload.include?._count?.select, - kind: 'include' as const, - isCount: true, - }), - ]; - - for (const selector of selectors) { - const { data, kind, isCount } = selector(args); - if (!data || typeof data !== 'object') { - continue; - } - - for (const [field, value] of Object.entries(data)) { - const fieldInfo = resolveField(this.options.modelMeta, model, field); - if (!fieldInfo) { - continue; - } - - if (this.isDelegateOrDescendantOfDelegate(fieldInfo?.type) && value) { - // delegate model, recursively inject hierarchy - if (data[field]) { - if (data[field] === true) { - // make sure the payload is an object - data[field] = {}; - } - await this.injectSelectIncludeHierarchy(fieldInfo.type, data[field]); - if (data[field].where) { - this.injectWhereHierarchy(fieldInfo.type, data[field].where); - } - } - } - - // refetch the field select/include value because it may have been - // updated during injection - const fieldValue = data[field]; - - if (fieldValue !== undefined) { - if (fieldValue.orderBy) { - // `orderBy` may contain fields from base types - enumerate(fieldValue.orderBy).forEach((item) => - this.injectWhereHierarchy(fieldInfo.type, item) - ); - } - - let injected = false; - if (!isCount) { - // regular select/include injection - injected = await this.injectBaseFieldSelect(model, field, fieldValue, args, kind); - if (injected) { - // if injected, remove the field from the original payload - delete data[field]; - } - } else { - // _count select/include injection, inject into an empty payload and then merge to the proper location - const injectTarget = { [kind]: {} }; - injected = await this.injectBaseFieldSelect(model, field, fieldValue, injectTarget, kind, true); - if (injected) { - // if injected, remove the field from the original payload - delete data[field]; - if (Object.keys(data).length === 0) { - // if the original "_count" payload becomes empty, remove it - delete args[kind]['_count']; - } - // finally merge the injection into the original payload - const merged = deepmerge(args[kind], injectTarget[kind]); - args[kind] = merged; - } - } - - if (!injected && fieldInfo.isDataModel) { - let nextValue = fieldValue; - if (nextValue === true) { - // make sure the payload is an object - data[field] = nextValue = {}; - } - await this.injectSelectIncludeHierarchy(fieldInfo.type, nextValue); - } - } - } - } - - if (!args.select) { - // include base models upwards - this.injectBaseIncludeRecursively(model, args); - - // include sub models downwards - await this.injectConcreteIncludeRecursively(model, args); - } - } - - private async buildSelectIncludeHierarchy(model: string, args: any, includeConcreteFields = true) { - args = clone(args); - const selectInclude: any = this.extractSelectInclude(args) || {}; - - if (selectInclude.select && typeof selectInclude.select === 'object') { - Object.entries(selectInclude.select).forEach(([field, value]) => { - if (value) { - if (this.injectBaseFieldSelect(model, field, value, selectInclude, 'select')) { - delete selectInclude.select[field]; - } - } - }); - } else if (selectInclude.include && typeof selectInclude.include === 'object') { - Object.entries(selectInclude.include).forEach(([field, value]) => { - if (value) { - if (this.injectBaseFieldSelect(model, field, value, selectInclude, 'include')) { - delete selectInclude.include[field]; - } - } - }); - } - - if (!selectInclude.select) { - this.injectBaseIncludeRecursively(model, selectInclude); - - if (includeConcreteFields) { - await this.injectConcreteIncludeRecursively(model, selectInclude); - } - } - return selectInclude; - } - - private injectBaseFieldSelect( - model: string, - field: string, - value: any, - selectInclude: any, - context: 'select' | 'include', - forCount = false // if the injection is for a "{ _count: { select: { field: true } } }" payload - ) { - const fieldInfo = resolveField(this.options.modelMeta, model, field); - if (!fieldInfo?.inheritedFrom) { - return false; - } - - let base = this.getBaseModel(model); - let target = selectInclude; - - while (base) { - const baseRelationName = this.makeAuxRelationName(base); - - // prepare base layer select/include - let thisLayer: any; - if (target.include) { - thisLayer = target.include; - } else if (target.select) { - thisLayer = target.select; - } else { - thisLayer = target.select = {}; - } - - if (base.name === fieldInfo.inheritedFrom) { - if (!thisLayer[baseRelationName]) { - thisLayer[baseRelationName] = { [context]: {} }; - } - if (forCount) { - // { _count: { select: { field: true } } } => { delegate_aux_[Base]: { select: { _count: { select: { field: true } } } } } - if ( - !thisLayer[baseRelationName][context]['_count'] || - typeof thisLayer[baseRelationName][context] !== 'object' - ) { - thisLayer[baseRelationName][context]['_count'] = {}; - } - thisLayer[baseRelationName][context]['_count'] = deepmerge( - thisLayer[baseRelationName][context]['_count'], - { select: { [field]: value } } - ); - } else { - // { select: { field: true } } => { delegate_aux_[Base]: { select: { field: true } } } - thisLayer[baseRelationName][context][field] = value; - } - break; - } else { - if (!thisLayer[baseRelationName]) { - thisLayer[baseRelationName] = { select: {} }; - } - target = thisLayer[baseRelationName]; - base = this.getBaseModel(base.name); - } - } - - return true; - } - - private injectBaseIncludeRecursively(model: string, selectInclude: any) { - const base = this.getBaseModel(model); - if (!base) { - return; - } - const baseRelationName = this.makeAuxRelationName(base); - - if (selectInclude.select) { - selectInclude.include = { [baseRelationName]: {}, ...selectInclude.select }; - delete selectInclude.select; - } else { - selectInclude.include = { [baseRelationName]: {}, ...selectInclude.include }; - } - this.injectBaseIncludeRecursively(base.name, selectInclude.include[baseRelationName]); - } - - private async injectConcreteIncludeRecursively(model: string, selectInclude: any) { - const modelInfo = getModelInfo(this.options.modelMeta, model); - if (!modelInfo) { - return; - } - - // get sub models of this model - const subModels = Object.values(this.options.modelMeta.models).filter((m) => - m.baseTypes?.includes(modelInfo.name) - ); - - for (const subModel of subModels) { - // include sub model relation field - const subRelationName = this.makeAuxRelationName(subModel); - - // create a payload to include the sub model relation - const includePayload = await this.createConcreteRelationIncludePayload(subModel.name); - - if (selectInclude.select) { - selectInclude.include = { [subRelationName]: includePayload, ...selectInclude.select }; - delete selectInclude.select; - } else { - selectInclude.include = { [subRelationName]: includePayload, ...selectInclude.include }; - } - await this.injectConcreteIncludeRecursively(subModel.name, selectInclude.include[subRelationName]); - } - } - - private async createConcreteRelationIncludePayload(model: string) { - let result: any = {}; - - if (this.options.processIncludeRelationPayload) { - // use the callback in options to process the include payload, so enhancements - // like 'policy' can do extra work (e.g., inject policy rules) - - // TODO: this causes both delegate base's policy rules and concrete model's rules to be injected, - // which is not wrong but redundant - - await this.options.processIncludeRelationPayload(this.prisma, model, result, this.options, this.context); - - const properSelectIncludeHierarchy = await this.buildSelectIncludeHierarchy(model, result, false); - result = { ...result, ...properSelectIncludeHierarchy }; - } - - return result; - } - - // #endregion - - // #region create - - override async create(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - 'data field is required in query argument' - ); - } - - this.sanitizeMutationPayload(args.data); - - if (isDelegateModel(this.options.modelMeta, this.model)) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - `Model "${this.model}" is a delegate and cannot be created directly` - ); - } - - if (!this.involvesDelegateModel(this.model)) { - return super.create(args); - } - - return this.doCreate(this.prisma, this.model, args); - } - - private sanitizeMutationPayload(data: any) { - if (!data) { - return; - } - - const prisma = this.prisma; - const prismaModule = this.options.prismaModule; - simpleTraverse(data, ({ key }) => { - if (key?.startsWith(DELEGATE_AUX_RELATION_PREFIX)) { - throw prismaClientValidationError( - prisma, - prismaModule, - `Auxiliary relation field "${key}" cannot be set directly` - ); - } - }); - } - - override createMany(args: { data: any; skipDuplicates?: boolean }): Promise<{ count: number }> { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - 'data field is required in query argument' - ); - } - - this.sanitizeMutationPayload(args.data); - - if (!this.involvesDelegateModel(this.model)) { - return super.createMany(args); - } - - if (this.isDelegateOrDescendantOfDelegate(this.model) && args.skipDuplicates) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - '`createMany` with `skipDuplicates` set to true is not supported for delegated models' - ); - } - - // `createMany` doesn't support nested create, which is needed for creating entities - // inheriting a delegate base, so we need to convert it to a regular `create` here. - // Note that the main difference is `create` doesn't support `skipDuplicates` as - // `createMany` does. - - return this.queryUtils.transaction(this.prisma, async (tx) => { - const r = await Promise.all( - enumerate(args.data).map(async (item) => { - return this.doCreate(tx, this.model, { data: item }); - }) - ); - return { count: r.length }; - }); - } - - override createManyAndReturn(args: { data: any; select?: any; skipDuplicates?: boolean }): Promise { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - 'data field is required in query argument' - ); - } - - this.sanitizeMutationPayload(args.data); - - if (!this.involvesDelegateModel(this.model)) { - return super.createManyAndReturn(args); - } - - if (this.isDelegateOrDescendantOfDelegate(this.model) && args.skipDuplicates) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - '`createManyAndReturn` with `skipDuplicates` set to true is not supported for delegated models' - ); - } - - // `createManyAndReturn` doesn't support nested create, which is needed for creating entities - // inheriting a delegate base, so we need to convert it to a regular `create` here. - // Note that the main difference is `create` doesn't support `skipDuplicates` as - // `createManyAndReturn` does. - - return this.queryUtils.transaction(this.prisma, async (tx) => { - const r = await Promise.all( - enumerate(args.data).map(async (item) => { - return this.doCreate(tx, this.model, { data: item, select: args.select }); - }) - ); - return r; - }); - } - - private async doCreate(db: CrudContract, model: string, args: any) { - args = clone(args); - - await this.injectCreateHierarchy(model, args); - await this.injectSelectIncludeHierarchy(model, args); - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`create\` ${this.getModelName(model)}: ${formatObject(args)}`); - } - const result = await db[model].create(args); - return this.assembleHierarchy(model, result); - } - - private async injectCreateHierarchy(model: string, args: any) { - const visitor = new NestedWriteVisitor(this.options.modelMeta, { - create: (model, args, _context) => { - this.doProcessCreatePayload(model, args); - }, - - createMany: (model, args, context) => { - // `createMany` doesn't support nested create, which is needed for creating entities - // inheriting a delegate base, so we need to convert it to a regular `create` here. - // Note that the main difference is `create` doesn't support `skipDuplicates` as - // `createMany` does. - - if (this.isDelegateOrDescendantOfDelegate(model)) { - if (args.skipDuplicates) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - '`createMany` with `skipDuplicates` set to true is not supported for delegated models' - ); - } - - // convert to regular `create` - let createPayload = context.parent.create ?? []; - if (!Array.isArray(createPayload)) { - createPayload = [createPayload]; - } - - for (const item of enumerate(args.data)) { - this.doProcessCreatePayload(model, item); - createPayload.push(item); - } - context.parent.create = createPayload; - delete context.parent['createMany']; - } - }, - }); - - await visitor.visit(model, 'create', args); - } - - private doProcessCreatePayload(model: string, args: any) { - if (!args) { - return; - } - - this.ensureBaseCreateHierarchy(model, args); - - for (const [field, value] of Object.entries(args)) { - const fieldInfo = resolveField(this.options.modelMeta, model, field); - if (fieldInfo?.inheritedFrom) { - this.injectBaseFieldData(model, fieldInfo, value, args, 'create'); - delete args[field]; - } - } - } - - // ensure the full nested "create" structure is created for base types - private ensureBaseCreateHierarchy(model: string, args: any) { - let curr = args; - let base = this.getBaseModel(model); - let sub = this.getModelInfo(model); - const hasDelegateBase = !!base; - - while (base) { - const baseRelationName = this.makeAuxRelationName(base); - - if (!curr[baseRelationName]) { - curr[baseRelationName] = {}; - } - if (!curr[baseRelationName].create) { - curr[baseRelationName].create = {}; - if (base.discriminator) { - // set discriminator field - curr[baseRelationName].create[base.discriminator] = sub.name; - } - } - - // Look for base id field assignments in the current level, and push - // them down to the base level - for (const idField of getIdFields(this.options.modelMeta, base.name)) { - if (curr[idField.name] !== undefined) { - curr[baseRelationName].create[idField.name] = curr[idField.name]; - delete curr[idField.name]; - } - } - - curr = curr[baseRelationName].create; - sub = base; - base = this.getBaseModel(base.name); - } - - if (hasDelegateBase) { - // A delegate base model creation is added, this can be incompatible if - // the user-provided payload assigns foreign keys directly, because Prisma - // doesn't permit mixed "checked" and "unchecked" fields in a payload. - // - // { - // delegate_aux_base: { ... }, - // [fkField]: value // <- this is not compatible - // } - // - // We need to convert foreign key assignments to `connect`. - this.fkAssignmentToConnect(model, args); - } - } - - // convert foreign key assignments to `connect` payload - // e.g.: { authorId: value } -> { author: { connect: { id: value } } } - private fkAssignmentToConnect(model: string, args: any) { - const keysToDelete: string[] = []; - for (const [key, value] of Object.entries(args)) { - if (value === undefined) { - continue; - } - - const fieldInfo = this.queryUtils.getModelField(model, key); - if ( - !fieldInfo?.inheritedFrom && // fields from delegate base are handled outside - fieldInfo?.isForeignKey - ) { - const relationInfo = this.queryUtils.getRelationForForeignKey(model, key); - if (relationInfo) { - // turn { [fk]: value } into { [relation]: { connect: { [id]: value } } } - const relationName = relationInfo.relation.name; - if (!args[relationName]) { - args[relationName] = {}; - } - if (!args[relationName].connect) { - args[relationName].connect = {}; - } - if (!(relationInfo.idField in args[relationName].connect)) { - args[relationName].connect[relationInfo.idField] = value; - keysToDelete.push(key); - } - } - } - } - - keysToDelete.forEach((key) => delete args[key]); - } - - // inject field data that belongs to base type into proper nesting structure - private injectBaseFieldData( - model: string, - fieldInfo: FieldInfo, - value: unknown, - args: any, - mode: 'create' | 'update' - ) { - let base = this.getBaseModel(model); - let curr = args; - - while (base) { - if (base.discriminator === fieldInfo.name) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - `fields "${fieldInfo.name}" is a discriminator and cannot be set directly` - ); - } - - const baseRelationName = this.makeAuxRelationName(base); - - if (!curr[baseRelationName]) { - curr[baseRelationName] = {}; - } - if (!curr[baseRelationName][mode]) { - curr[baseRelationName][mode] = {}; - } - curr = curr[baseRelationName][mode]; - - if (fieldInfo.inheritedFrom === base.name) { - curr[fieldInfo.name] = value; - break; - } - - base = this.getBaseModel(base.name); - } - } - - // #endregion - - // #region update - - override update(args: any): Promise { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - 'data field is required in query argument' - ); - } - - this.sanitizeMutationPayload(args.data); - - if (!this.involvesDelegateModel(this.model)) { - return super.update(args); - } - - return this.queryUtils.transaction(this.prisma, (tx) => this.doUpdate(tx, this.model, args)); - } - - override async updateMany(args: any): Promise<{ count: number }> { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - 'data field is required in query argument' - ); - } - - this.sanitizeMutationPayload(args.data); - - if (!this.involvesDelegateModel(this.model)) { - return super.updateMany(args); - } - - let simpleUpdateMany = Object.keys(args.data).every((key) => { - // check if the `data` clause involves base fields - const fieldInfo = resolveField(this.options.modelMeta, this.model, key); - return !fieldInfo?.inheritedFrom; - }); - - // check if there are any `@updatedAt` fields from delegate base models - if (simpleUpdateMany) { - if (this.getUpdatedAtFromDelegateBases(this.model).length > 0) { - simpleUpdateMany = false; - } - } - - return this.queryUtils.transaction(this.prisma, (tx) => - this.doUpdateMany(tx, this.model, args, simpleUpdateMany) - ); - } - - override async upsert(args: any): Promise { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - if (!args.where) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - 'where field is required in query argument' - ); - } - - this.sanitizeMutationPayload(args.update); - this.sanitizeMutationPayload(args.create); - - if (isDelegateModel(this.options.modelMeta, this.model)) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - `Model "${this.model}" is a delegate and doesn't support upsert` - ); - } - - if (!this.involvesDelegateModel(this.model)) { - return super.upsert(args); - } - - args = clone(args); - this.injectWhereHierarchy(this.model, (args as any)?.where); - await this.injectSelectIncludeHierarchy(this.model, args); - if (args.create) { - this.doProcessCreatePayload(this.model, args.create); - } - if (args.update) { - this.doProcessUpdatePayload(this.model, args.update); - } - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`upsert\` ${this.getModelName(this.model)}: ${formatObject(args)}`); - } - const result = await this.prisma[this.model].upsert(args); - return this.assembleHierarchy(this.model, result); - } - - private async doUpdate(db: CrudContract, model: string, args: any): Promise { - args = clone(args); - - await this.injectUpdateHierarchy(db, model, args); - await this.injectSelectIncludeHierarchy(model, args); - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`update\` ${this.getModelName(model)}: ${formatObject(args)}`); - } - const result = await db[model].update(args); - return this.assembleHierarchy(model, result); - } - - private async doUpdateMany( - db: CrudContract, - model: string, - args: any, - simpleUpdateMany: boolean - ): Promise<{ count: number }> { - if (simpleUpdateMany) { - // do a direct `updateMany` - args = clone(args); - await this.injectUpdateHierarchy(db, model, args); - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`updateMany\` ${this.getModelName(model)}: ${formatObject(args)}`); - } - return db[model].updateMany(args); - } else { - // translate to plain `update` for nested write into base fields - const findArgs = { - where: clone(args.where ?? {}), - select: this.queryUtils.makeIdSelection(model), - }; - await this.injectUpdateHierarchy(db, model, findArgs); - if (this.options.logPrismaQuery) { - this.logger.info( - `[delegate] \`updateMany\` find candidates: ${this.getModelName(model)}: ${formatObject(findArgs)}` - ); - } - const entities = await db[model].findMany(findArgs); - - const updatePayload = { data: clone(args.data), select: this.queryUtils.makeIdSelection(model) }; - await this.injectUpdateHierarchy(db, model, updatePayload); - const result = await Promise.all( - entities.map((entity) => { - const updateArgs = { - where: entity, - ...updatePayload, - }; - if (this.options.logPrismaQuery) { - this.logger.info( - `[delegate] \`updateMany\` update: ${this.getModelName(model)}: ${formatObject(updateArgs)}` - ); - } - return db[model].update(updateArgs); - }) - ); - return { count: result.length }; - } - } - - private async injectUpdateHierarchy(db: CrudContract, model: string, args: any) { - const visitor = new NestedWriteVisitor(this.options.modelMeta, { - update: (model, args, _context) => { - this.injectWhereHierarchy(model, (args as any)?.where); - this.doProcessUpdatePayload(model, (args as any)?.data); - }, - - updateMany: async (model, args, context) => { - let simpleUpdateMany = Object.keys(args.data).every((key) => { - // check if the `data` clause involves base fields - const fieldInfo = resolveField(this.options.modelMeta, model, key); - return !fieldInfo?.inheritedFrom; - }); - - // check if there are any `@updatedAt` fields from delegate base models - if (simpleUpdateMany) { - if (this.getUpdatedAtFromDelegateBases(model).length > 0) { - simpleUpdateMany = false; - } - } - - if (simpleUpdateMany) { - // check if the `where` clause involves base fields - simpleUpdateMany = Object.keys(args.where || {}).every((key) => { - const fieldInfo = resolveField(this.options.modelMeta, model, key); - return !fieldInfo?.inheritedFrom; - }); - } - - if (simpleUpdateMany) { - this.injectWhereHierarchy(model, (args as any)?.where); - this.doProcessUpdatePayload(model, (args as any)?.data); - } else { - const where = await this.queryUtils.buildReversedQuery(db, context, false, false); - await this.queryUtils.transaction(db, async (tx) => { - await this.doUpdateMany(tx, model, { ...args, where }, simpleUpdateMany); - }); - delete context.parent['updateMany']; - } - }, - - upsert: (model, args, _context) => { - this.injectWhereHierarchy(model, (args as any)?.where); - if (args.create) { - this.doProcessCreatePayload(model, (args as any)?.create); - } - if (args.update) { - this.doProcessUpdatePayload(model, (args as any)?.update); - } - }, - - create: (model, args, _context) => { - if (isDelegateModel(this.options.modelMeta, model)) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - `Model "${model}" is a delegate and cannot be created directly` - ); - } - this.doProcessCreatePayload(model, args); - }, - - createMany: (model, args, _context) => { - if (args.skipDuplicates) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - '`createMany` with `skipDuplicates` set to true is not supported for delegated models' - ); - } - - for (const item of enumerate(args?.data)) { - this.doProcessCreatePayload(model, item); - } - }, - - connect: (model, args, _context) => { - this.injectWhereHierarchy(model, args); - }, - - connectOrCreate: (model, args, _context) => { - this.injectWhereHierarchy(model, args.where); - if (args.create) { - this.doProcessCreatePayload(model, args.create); - } - }, - - disconnect: (model, args, _context) => { - this.injectWhereHierarchy(model, args); - }, - - set: (model, args, _context) => { - this.injectWhereHierarchy(model, args); - }, - - delete: async (model, _args, context) => { - const where = await this.queryUtils.buildReversedQuery(db, context, false, false); - await this.queryUtils.transaction(db, async (tx) => { - await this.doDelete(tx, model, { where }); - }); - delete context.parent['delete']; - }, - - deleteMany: async (model, _args, context) => { - const where = await this.queryUtils.buildReversedQuery(db, context, false, false); - await this.queryUtils.transaction(db, async (tx) => { - await this.doDeleteMany(tx, model, where); - }); - delete context.parent['deleteMany']; - }, - }); - - await visitor.visit(model, 'update', args); - } - - private doProcessUpdatePayload(model: string, data: any) { - if (!data) { - return; - } - - for (const [field, value] of Object.entries(data)) { - const fieldInfo = resolveField(this.options.modelMeta, model, field); - if (fieldInfo?.inheritedFrom) { - this.injectBaseFieldData(model, fieldInfo, value, data, 'update'); - delete data[field]; - } - } - - // if we're updating any field, we need to take care of updating `@updatedAt` - // fields inherited from delegate base models - if (Object.keys(data).length > 0) { - const updatedAtFields = this.getUpdatedAtFromDelegateBases(model); - for (const fieldInfo of updatedAtFields) { - this.injectBaseFieldData(model, fieldInfo, new Date(), data, 'update'); - } - } - } - - // #endregion - - // #region delete - - override delete(args: any): Promise { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - - if (!this.involvesDelegateModel(this.model)) { - return super.delete(args); - } - - return this.queryUtils.transaction(this.prisma, async (tx) => { - const selectInclude = await this.buildSelectIncludeHierarchy(this.model, args); - - // make sure id fields are selected - const idFields = this.getIdFields(this.model); - for (const idField of idFields) { - if (selectInclude?.select && !(idField.name in selectInclude.select)) { - selectInclude.select[idField.name] = true; - } - } - - const deleteArgs = { ...clone(args), ...selectInclude }; - return this.doDelete(tx, this.model, deleteArgs); - }); - } - - override deleteMany(args: any): Promise<{ count: number }> { - if (!this.involvesDelegateModel(this.model)) { - return super.deleteMany(args); - } - - return this.queryUtils.transaction(this.prisma, (tx) => this.doDeleteMany(tx, this.model, args?.where)); - } - - private async doDeleteMany(db: CrudContract, model: string, where: any): Promise<{ count: number }> { - // query existing entities with id - const idSelection = this.queryUtils.makeIdSelection(model); - const findArgs = { where: clone(where ?? {}), select: idSelection }; - this.injectWhereHierarchy(model, findArgs.where); - - if (this.options.logPrismaQuery) { - this.logger.info( - `[delegate] \`deleteMany\` find candidates: ${this.getModelName(model)}: ${formatObject(findArgs)}` - ); - } - const entities = await db[model].findMany(findArgs); - - // recursively delete base entities (they all have the same id values) - - await Promise.all( - entities.map((entity) => { - let deleteFilter = entity; - if (Object.keys(deleteFilter).length > 1) { - // if the model has compound id fields, we need to compose a compound key filter, - // otherwise calling Prisma's `delete` won't work - deleteFilter = this.queryUtils.composeCompoundUniqueField(model, deleteFilter); - } - return this.doDelete(db, model, { where: deleteFilter }); - }) - ); - - return { count: entities.length }; - } - - private async deleteBaseRecursively(db: CrudContract, model: string, idValues: any) { - let base = this.getBaseModel(model); - while (base) { - let deleteFilter = idValues; - if (Object.keys(idValues).length > 1) { - // if the model has compound id fields, we need to compose a compound key filter, - // otherwise calling Prisma's `delete` won't work - deleteFilter = this.queryUtils.composeCompoundUniqueField(base.name, deleteFilter); - } - await db[base.name].delete({ where: deleteFilter }); - base = this.getBaseModel(base.name); - } - } - - private async doDelete(db: CrudContract, model: string, args: any, readBack = true): Promise { - this.injectWhereHierarchy(model, args.where); - await this.injectSelectIncludeHierarchy(model, args); - - // read relation entities that need to be cascade deleted before deleting the main entity - const cascadeDeletes = await this.getRelationDelegateEntitiesForCascadeDelete(db, model, args.where); - - let result: unknown = undefined; - if (cascadeDeletes.length > 0) { - // we'll need to do cascade deletes of relations, so first - // read the current entity before anything changes - if (readBack) { - result = await this.doFind(db, model, 'findUnique', args); - } - - // process cascade deletes of relations, this ensure their delegate base - // entities are deleted as well - await Promise.all( - cascadeDeletes.map(({ model, entity }) => this.doDelete(db, model, { where: entity }, false)) - ); - } - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`delete\` ${this.getModelName(model)}: ${formatObject(args)}`); - } - - const deleteResult = await db[model].delete(args); - if (!result) { - result = this.assembleHierarchy(model, deleteResult); - } - - // recursively delete base entities (they all have the same id values) - const idValues = this.queryUtils.getEntityIds(model, deleteResult); - await this.deleteBaseRecursively(db, model, idValues); - - return result; - } - - private async getRelationDelegateEntitiesForCascadeDelete(db: CrudContract, model: string, where: any) { - if (!where || Object.keys(where).length === 0) { - throw new Error('where clause is required for cascade delete'); - } - - const cascadeDeletes: Array<{ model: string; entity: any }> = []; - const fields = getFields(this.options.modelMeta, model); - if (fields) { - for (const fieldInfo of Object.values(fields)) { - if (!fieldInfo.isDataModel) { - continue; - } - - if (fieldInfo.isRelationOwner) { - // this side of the relation owns the foreign key, - // so it won't cause cascade delete to the other side - continue; - } - - if (fieldInfo.backLink) { - // get the opposite side of the relation - const backLinkField = this.queryUtils.getModelField(fieldInfo.type, fieldInfo.backLink); - - if (backLinkField?.isRelationOwner && this.isFieldCascadeDelete(backLinkField)) { - // if the opposite side of the relation is to be cascade deleted, - // recursively delete the delegate base entities - const relationModel = getModelInfo(this.options.modelMeta, fieldInfo.type); - if (relationModel?.baseTypes && relationModel.baseTypes.length > 0) { - // the relation model has delegate base, cascade the delete to the base - const relationEntities = await db[relationModel.name].findMany({ - where: { [backLinkField.name]: where }, - select: this.queryUtils.makeIdSelection(relationModel.name), - }); - relationEntities.forEach((entity) => { - cascadeDeletes.push({ model: fieldInfo.type, entity }); - }); - } - } - } - } - } - return cascadeDeletes; - } - - private isFieldCascadeDelete(fieldInfo: FieldInfo) { - return fieldInfo.onDeleteAction === 'Cascade'; - } - - // #endregion - - // #region aggregation - - override aggregate(args: any): Promise { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - if (!this.involvesDelegateModel(this.model)) { - return super.aggregate(args); - } - - // check if any aggregation operator is using fields from base - this.checkAggregationArgs('aggregate', args); - - args = clone(args); - - if (args.cursor) { - this.injectWhereHierarchy(this.model, args.cursor); - } - - if (args.orderBy) { - enumerate(args.orderBy).forEach((item) => this.injectWhereHierarchy(this.model, item)); - } - - if (args.where) { - this.injectWhereHierarchy(this.model, args.where); - } - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`aggregate\` ${this.getModelName(this.model)}: ${formatObject(args)}`); - } - return super.aggregate(args); - } - - override count(args: any): Promise { - if (!this.involvesDelegateModel(this.model)) { - return super.count(args); - } - - // check if count select is using fields from base - this.checkAggregationArgs('count', args); - - args = clone(args); - - if (args?.cursor) { - this.injectWhereHierarchy(this.model, args.cursor); - } - - if (args?.where) { - this.injectWhereHierarchy(this.model, args.where); - } - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`count\` ${this.getModelName(this.model)}: ${formatObject(args)}`); - } - return super.count(args); - } - - override groupBy(args: any): Promise { - if (!args) { - throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required'); - } - if (!this.involvesDelegateModel(this.model)) { - return super.groupBy(args); - } - - // check if count select is using fields from base - this.checkAggregationArgs('groupBy', args); - - if (args.by) { - for (const by of enumerate(args.by)) { - const fieldInfo = resolveField(this.options.modelMeta, this.model, by); - if (fieldInfo && fieldInfo.inheritedFrom) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - `groupBy with fields from base type is not supported yet: "${by}"` - ); - } - } - } - - args = clone(args); - - if (args.where) { - this.injectWhereHierarchy(this.model, args.where); - } - - if (this.options.logPrismaQuery) { - this.logger.info(`[delegate] \`groupBy\` ${this.getModelName(this.model)}: ${formatObject(args)}`); - } - return super.groupBy(args); - } - - private checkAggregationArgs(operation: 'aggregate' | 'count' | 'groupBy', args: any) { - if (!args) { - return; - } - - for (const op of ['_count', '_sum', '_avg', '_min', '_max', 'select', 'having']) { - if (args[op] && typeof args[op] === 'object') { - for (const field of Object.keys(args[op])) { - const fieldInfo = resolveField(this.options.modelMeta, this.model, field); - if (fieldInfo?.inheritedFrom) { - throw prismaClientValidationError( - this.prisma, - this.options.prismaModule, - `${operation} with fields from base type is not supported yet: "${field}"` - ); - } - } - } - } - } - - // #endregion - - // #region utils - - private extractSelectInclude(args: any) { - if (!args) { - return undefined; - } - args = clone(args); - return 'select' in args - ? { select: args['select'] } - : 'include' in args - ? { include: args['include'] } - : undefined; - } - - private makeAuxRelationName(model: ModelInfo) { - const name = `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(model.name)}`; - // make sure we look up into short name map to see if it's truncated - const shortName = this.options.modelMeta.shortNameMap?.[name]; - return shortName ?? name; - } - - private getModelName(model: string) { - const info = getModelInfo(this.options.modelMeta, model, true); - return info.name; - } - - private getIdFields(model: string): FieldInfo[] { - const idFields = getIdFields(this.options.modelMeta, model); - if (idFields && idFields.length > 0) { - return idFields; - } - const base = this.getBaseModel(model); - return base ? this.getIdFields(base.name) : []; - } - - private getModelInfo(model: string) { - return getModelInfo(this.options.modelMeta, model, true); - } - - private getBaseModel(model: string) { - const baseNames = getModelInfo(this.options.modelMeta, model, true).baseTypes; - if (!baseNames) { - return undefined; - } - if (baseNames.length > 1) { - throw new Error('Multi-inheritance is not supported'); - } - return this.options.modelMeta.models[lowerCaseFirst(baseNames[0])]; - } - - private involvesDelegateModel(model: string, visited?: Set): boolean { - if (this.isDelegateOrDescendantOfDelegate(model)) { - return true; - } - - visited = visited ?? new Set(); - if (visited.has(model)) { - return false; - } - visited.add(model); - - const modelInfo = getModelInfo(this.options.modelMeta, model, true); - return Object.values(modelInfo.fields).some( - (field) => field.isDataModel && this.involvesDelegateModel(field.type, visited) - ); - } - - private isDelegateOrDescendantOfDelegate(model: string): boolean { - if (isDelegateModel(this.options.modelMeta, model)) { - return true; - } - const baseTypes = getModelInfo(this.options.modelMeta, model)?.baseTypes; - return !!( - baseTypes && - baseTypes.length > 0 && - baseTypes.some((base) => this.isDelegateOrDescendantOfDelegate(base)) - ); - } - - private assembleHierarchy(model: string, entity: any) { - if (!entity || typeof entity !== 'object') { - return entity; - } - - const upMerged = this.assembleUp(model, entity); - const downMerged = this.assembleDown(model, entity); - - // https://www.npmjs.com/package/deepmerge#arraymerge-example-combine-arrays - const combineMerge = (target: any[], source: any[], options: ArrayMergeOptions) => { - const destination = target.slice(); - source.forEach((item, index) => { - if (typeof destination[index] === 'undefined') { - destination[index] = options.cloneUnlessOtherwiseSpecified(item, options); - } else if (options.isMergeableObject(item)) { - destination[index] = deepmerge(target[index], item, options); - } else if (target.indexOf(item) === -1) { - destination.push(item); - } - }); - return destination; - }; - - const result: any = deepmerge(upMerged, downMerged, { - arrayMerge: combineMerge, - isMergeableObject: (v) => isPlainObject(v) || Array.isArray(v), // avoid messing with Decimal, Date, etc. - }); - return result; - } - - private assembleUp(model: string, entity: any) { - if (!entity) { - return entity; - } - - const result: any = {}; - const base = this.getBaseModel(model); - - if (base) { - // fully merge base fields - const baseRelationName = this.makeAuxRelationName(base); - const baseData = entity[baseRelationName]; - if (baseData && typeof baseData === 'object') { - const baseAssembled = this.assembleHierarchy(base.name, baseData); - Object.assign(result, baseAssembled); - } - } - - const modelInfo = getModelInfo(this.options.modelMeta, model, true); - - for (const [key, value] of Object.entries(entity)) { - if (key.startsWith(DELEGATE_AUX_RELATION_PREFIX)) { - continue; - } - - const field = modelInfo.fields[key]; - if (!field) { - // not a field, could be `_count`, `_sum`, etc. - result[key] = value; - continue; - } - - if (field.inheritedFrom) { - // already merged from base - continue; - } - - if (field.isDataModel) { - if (Array.isArray(value)) { - result[field.name] = value.map((item) => this.assembleUp(field.type, item)); - } else { - result[field.name] = this.assembleUp(field.type, value); - } - } else { - result[field.name] = value; - } - } - - return result; - } - - private assembleDown(model: string, entity: any) { - if (!entity) { - return entity; - } - - const result: any = {}; - const modelInfo = getModelInfo(this.options.modelMeta, model, true); - - if (modelInfo.discriminator) { - // model is a delegate, fully merge concrete model fields - const subModelName = entity[modelInfo.discriminator]; - if (subModelName) { - const subModel = getModelInfo(this.options.modelMeta, subModelName, true); - const subRelationName = this.makeAuxRelationName(subModel); - const subData = entity[subRelationName]; - if (subData && typeof subData === 'object') { - const subAssembled = this.assembleHierarchy(subModel.name, subData); - Object.assign(result, subAssembled); - } - } - } - - for (const [key, value] of Object.entries(entity)) { - if (key.startsWith(DELEGATE_AUX_RELATION_PREFIX)) { - continue; - } - - const field = modelInfo.fields[key]; - if (!field) { - // not a field, could be `_count`, `_sum`, etc. - result[key] = value; - continue; - } - - if (field.isDataModel) { - if (Array.isArray(value)) { - result[field.name] = value.map((item) => this.assembleDown(field.type, item)); - } else { - result[field.name] = this.assembleDown(field.type, value); - } - } else { - result[field.name] = value; - } - } - - return result; - } - - private getUpdatedAtFromDelegateBases(model: string) { - const result: FieldInfo[] = []; - const modelFields = getFields(this.options.modelMeta, model); - if (!modelFields) { - return result; - } - for (const fieldInfo of Object.values(modelFields)) { - if ( - fieldInfo.attributes?.some((attr) => attr.name === '@updatedAt') && - fieldInfo.inheritedFrom && - isDelegateModel(this.options.modelMeta, fieldInfo.inheritedFrom) - ) { - result.push(fieldInfo); - } - } - return result; - } - - // #endregion -} diff --git a/packages/runtime/src/enhancements/node/encryption.ts b/packages/runtime/src/enhancements/node/encryption.ts deleted file mode 100644 index 4859a1225..000000000 --- a/packages/runtime/src/enhancements/node/encryption.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import { ACTIONS_WITH_WRITE_PAYLOAD } from '../../constants'; -import { - FieldInfo, - NestedWriteVisitor, - enumerate, - getModelFields, - resolveField, - type PrismaWriteActionType, -} from '../../cross'; -import { Decrypter, Encrypter } from '../../encryption'; -import { CustomEncryption, DbClientContract, SimpleEncryption } from '../../types'; -import { InternalEnhancementOptions } from './create-enhancement'; -import { Logger } from './logger'; -import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; -import { QueryUtils } from './query-utils'; - -/** - * Gets an enhanced Prisma client that supports `@encrypted` attribute. - * - * @private - */ -export function withEncrypted( - prisma: DbClient, - options: InternalEnhancementOptions -): DbClient { - return makeProxy( - prisma, - options.modelMeta, - (_prisma, model) => new EncryptedHandler(_prisma as DbClientContract, model, options), - 'encryption' - ); -} - -class EncryptedHandler extends DefaultPrismaProxyHandler { - private queryUtils: QueryUtils; - private logger: Logger; - private encryptionKey: CryptoKey | undefined; - private encryptionKeyDigest: string | undefined; - private decryptionKeys: Array<{ key: CryptoKey; digest: string }> = []; - private encrypter: Encrypter | undefined; - private decrypter: Decrypter | undefined; - - constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { - super(prisma, model, options); - - this.queryUtils = new QueryUtils(prisma, options); - this.logger = new Logger(prisma); - - if (!options.encryption) { - throw this.queryUtils.unknownError('Encryption options must be provided'); - } - - if (this.isCustomEncryption(options.encryption!)) { - if (!options.encryption.encrypt || !options.encryption.decrypt) { - throw this.queryUtils.unknownError('Custom encryption must provide encrypt and decrypt functions'); - } - } else { - if (!options.encryption.encryptionKey) { - throw this.queryUtils.unknownError('Encryption key must be provided'); - } - - this.encrypter = new Encrypter(options.encryption.encryptionKey); - this.decrypter = new Decrypter([ - options.encryption.encryptionKey, - ...(options.encryption.decryptionKeys || []), - ]); - } - } - - private isCustomEncryption(encryption: CustomEncryption | SimpleEncryption): encryption is CustomEncryption { - return 'encrypt' in encryption && 'decrypt' in encryption; - } - - private async encrypt(field: FieldInfo, data: string): Promise { - if (this.isCustomEncryption(this.options.encryption!)) { - return this.options.encryption.encrypt(this.model, field, data); - } - - return this.encrypter!.encrypt(data); - } - - private async decrypt(field: FieldInfo, data: string): Promise { - if (this.isCustomEncryption(this.options.encryption!)) { - return this.options.encryption.decrypt(this.model, field, data); - } - - return this.decrypter!.decrypt(data); - } - - // base override - protected async preprocessArgs(action: PrismaProxyActions, args: any) { - if (args && ACTIONS_WITH_WRITE_PAYLOAD.includes(action)) { - await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args); - } - return args; - } - - // base override - protected async processResultEntity(method: PrismaProxyActions, data: T): Promise { - if (!data || typeof data !== 'object') { - return data; - } - - for (const value of enumerate(data)) { - await this.doPostProcess(value, this.model); - } - - return data; - } - - private async doPostProcess(entityData: any, model: string) { - const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData); - - for (const field of getModelFields(entityData)) { - // don't decrypt null, undefined or empty string values - if (!entityData[field]) continue; - - const fieldInfo = await resolveField(this.options.modelMeta, realModel, field); - if (!fieldInfo) { - continue; - } - - if (fieldInfo.isDataModel) { - const items = - fieldInfo.isArray && Array.isArray(entityData[field]) ? entityData[field] : [entityData[field]]; - for (const item of items) { - // recurse - await this.doPostProcess(item, fieldInfo.type); - } - } else { - const shouldDecrypt = fieldInfo.attributes?.find((attr) => attr.name === '@encrypted'); - if (shouldDecrypt) { - try { - entityData[field] = await this.decrypt(fieldInfo, entityData[field]); - } catch (error) { - this.logger.warn(`Decryption failed, keeping original value: ${error}`); - } - } - } - } - } - - private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) { - const visitor = new NestedWriteVisitor(this.options.modelMeta, { - field: async (field, _action, data, context) => { - // don't encrypt null, undefined or empty string values - if (!data) return; - - const encAttr = field.attributes?.find((attr) => attr.name === '@encrypted'); - if (encAttr && field.type === 'String') { - try { - context.parent[field.name] = await this.encrypt(field, data); - } catch (error) { - this.queryUtils.unknownError(`Encryption failed for field ${field.name}: ${error}`); - } - } - }, - }); - - await visitor.visit(model, action, args); - } -} diff --git a/packages/runtime/src/enhancements/node/index.ts b/packages/runtime/src/enhancements/node/index.ts deleted file mode 100644 index a6f0e9446..000000000 --- a/packages/runtime/src/enhancements/node/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from '../../cross'; -export * from './create-enhancement'; -export * from './types'; -export * from './utils'; diff --git a/packages/runtime/src/enhancements/node/json-processor.ts b/packages/runtime/src/enhancements/node/json-processor.ts deleted file mode 100644 index 6cf204a6f..000000000 --- a/packages/runtime/src/enhancements/node/json-processor.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { enumerate, getModelFields, resolveField } from '../../cross'; -import { DbClientContract } from '../../types'; -import { InternalEnhancementOptions } from './create-enhancement'; -import { DefaultPrismaProxyHandler, makeProxy, PrismaProxyActions } from './proxy'; -import { QueryUtils } from './query-utils'; - -/** - * Gets an enhanced Prisma client that post-processes JSON values. - * - * @private - */ -export function withJsonProcessor( - prisma: DbClient, - options: InternalEnhancementOptions -): DbClient { - return makeProxy( - prisma, - options.modelMeta, - (_prisma, model) => new JsonProcessorHandler(_prisma as DbClientContract, model, options), - 'json-processor' - ); -} - -class JsonProcessorHandler extends DefaultPrismaProxyHandler { - private queryUtils: QueryUtils; - - constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { - super(prisma, model, options); - this.queryUtils = new QueryUtils(prisma, options); - } - - protected override async processResultEntity(_method: PrismaProxyActions, data: T): Promise { - for (const value of enumerate(data)) { - await this.doPostProcess(value, this.model); - } - return data; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async doPostProcess(entityData: any, model: string) { - const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData); - - for (const field of getModelFields(entityData)) { - const fieldInfo = await resolveField(this.options.modelMeta, realModel, field); - if (!fieldInfo) { - continue; - } - - if (fieldInfo.isTypeDef) { - this.fixJsonDateFields(entityData[field], fieldInfo.type); - } else if (fieldInfo.isDataModel) { - const items = - fieldInfo.isArray && Array.isArray(entityData[field]) ? entityData[field] : [entityData[field]]; - for (const item of items) { - // recurse - await this.doPostProcess(item, fieldInfo.type); - } - } - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private fixJsonDateFields(entityData: any, typeDef: string) { - if (typeof entityData !== 'object' && !Array.isArray(entityData)) { - return; - } - - enumerate(entityData).forEach((item) => { - if (!item || typeof item !== 'object') { - return; - } - - for (const [key, value] of Object.entries(item)) { - const fieldInfo = resolveField(this.options.modelMeta, typeDef, key, true); - if (!fieldInfo) { - continue; - } - if (fieldInfo.isTypeDef) { - // recurse - this.fixJsonDateFields(value, fieldInfo.type); - } else if (fieldInfo.type === 'DateTime' && typeof value === 'string') { - // convert to Date - const parsed = Date.parse(value); - if (!isNaN(parsed)) { - item[key] = new Date(parsed); - } - } - } - }); - } -} diff --git a/packages/runtime/src/enhancements/node/logger.ts b/packages/runtime/src/enhancements/node/logger.ts deleted file mode 100644 index e53551563..000000000 --- a/packages/runtime/src/enhancements/node/logger.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { EventEmitter } from 'stream'; - -/** - * A logger that uses an existing Prisma client to emit. - */ -export class Logger { - private emitter: EventEmitter | undefined; - private eventNames: Array = []; - - constructor(private readonly prisma: any) { - const engine = (this.prisma as any)._engine; - this.emitter = engine ? (engine.logEmitter as EventEmitter) : undefined; - if (this.emitter) { - if (typeof this.emitter.eventNames === 'function') { - // Node.js - this.eventNames = this.emitter.eventNames(); - } else if ('events' in this.emitter && this.emitter.events && typeof this.emitter.events === 'object') { - // edge runtime - this.eventNames = Object.keys((this.emitter as any).events); - } else { - this.eventNames = []; - } - } - } - - /** - * Checks if a log level is enabled. - */ - public enabled(level: 'info' | 'warn' | 'error') { - return !!this.eventNames.includes(level); - } - - /** - * Generates a message with the given level. - */ - public log(level: 'info' | 'warn' | 'error', message: string) { - this.emitter?.emit(level, { - timestamp: new Date(), - message, - target: 'zenstack', - }); - } - - /** - * Generates a log message with info level. - */ - public info(message: string) { - this.log('info', message); - } - - /** - * Generates a log message with warn level. - */ - public warn(message: string) { - this.log('warn', message); - } - - /** - * Generates a log message with error level. - */ - public error(message: string) { - this.log('error', message); - } -} diff --git a/packages/runtime/src/enhancements/node/omit.ts b/packages/runtime/src/enhancements/node/omit.ts deleted file mode 100644 index 60e0251ac..000000000 --- a/packages/runtime/src/enhancements/node/omit.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { enumerate, getModelFields, resolveField } from '../../cross'; -import { DbClientContract } from '../../types'; -import { InternalEnhancementOptions } from './create-enhancement'; -import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; -import { QueryUtils } from './query-utils'; - -/** - * Gets an enhanced Prisma client that supports `@omit` attribute. - * - * @private - */ -export function withOmit(prisma: DbClient, options: InternalEnhancementOptions): DbClient { - return makeProxy( - prisma, - options.modelMeta, - (_prisma, model) => new OmitHandler(_prisma as DbClientContract, model, options), - 'omit' - ); -} - -class OmitHandler extends DefaultPrismaProxyHandler { - private queryUtils: QueryUtils; - - constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { - super(prisma, model, options); - this.queryUtils = new QueryUtils(prisma, options); - } - - // base override - protected async processResultEntity(method: string, data: T): Promise { - if (!data || typeof data !== 'object') { - return data; - } - - if (method === 'subscribe' || method === 'stream') { - if (!('action' in data)) { - return data; - } - - // Prisma Pulse result - switch (data.action) { - case 'create': - if ('created' in data) { - await this.doPostProcess(data.created, this.model); - } - break; - case 'update': - if ('before' in data) { - await this.doPostProcess(data.before, this.model); - } - if ('after' in data) { - await this.doPostProcess(data.after, this.model); - } - break; - case 'delete': - if ('deleted' in data) { - await this.doPostProcess(data.deleted, this.model); - } - break; - } - } else { - // regular prisma client result - for (const value of enumerate(data)) { - await this.doPostProcess(value, this.model); - } - } - return data; - } - - private async doPostProcess(entityData: any, model: string) { - const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData); - - for (const field of getModelFields(entityData)) { - const fieldInfo = await resolveField(this.options.modelMeta, realModel, field); - if (!fieldInfo) { - continue; - } - - const shouldOmit = fieldInfo.attributes?.find((attr) => attr.name === '@omit'); - if (shouldOmit) { - delete entityData[field]; - } - - if (fieldInfo.isDataModel) { - const items = - fieldInfo.isArray && Array.isArray(entityData[field]) ? entityData[field] : [entityData[field]]; - for (const item of items) { - // recurse - await this.doPostProcess(item, fieldInfo.type); - } - } - } - } -} diff --git a/packages/runtime/src/enhancements/node/password.ts b/packages/runtime/src/enhancements/node/password.ts deleted file mode 100644 index a2fdae42c..000000000 --- a/packages/runtime/src/enhancements/node/password.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import { ACTIONS_WITH_WRITE_PAYLOAD, DEFAULT_PASSWORD_SALT_LENGTH } from '../../constants'; -import { NestedWriteVisitor, type PrismaWriteActionType } from '../../cross'; -import { DbClientContract } from '../../types'; -import { InternalEnhancementOptions } from './create-enhancement'; -import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; - -/** - * Gets an enhanced Prisma client that supports `@password` attribute. - * - * @private - */ -export function withPassword( - prisma: DbClient, - options: InternalEnhancementOptions -): DbClient { - return makeProxy( - prisma, - options.modelMeta, - (_prisma, model) => new PasswordHandler(_prisma as DbClientContract, model, options), - 'password' - ); -} - -// `bcryptjs.hash` is good for performance but it doesn't work in vercel edge runtime, -// so we fall back to `bcrypt.hash` in that case. - -// eslint-disable-next-line no-var -declare var EdgeRuntime: any; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const hashFunc = typeof EdgeRuntime === 'string' ? require('bcryptjs').hashSync : require('bcryptjs').hash; - -class PasswordHandler extends DefaultPrismaProxyHandler { - constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) { - super(prisma, model, options); - } - - // base override - protected async preprocessArgs(action: PrismaProxyActions, args: any) { - if (args && ACTIONS_WITH_WRITE_PAYLOAD.includes(action)) { - await this.preprocessWritePayload(this.model, action as PrismaWriteActionType, args); - } - return args; - } - - private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) { - const visitor = new NestedWriteVisitor(this.options.modelMeta, { - field: async (field, _action, data, context) => { - const pwdAttr = field.attributes?.find((attr) => attr.name === '@password'); - if (pwdAttr && field.type === 'String') { - // hash password value - let salt: string | number | undefined = pwdAttr.args.find((arg) => arg.name === 'salt') - ?.value as string; - if (!salt) { - salt = pwdAttr.args.find((arg) => arg.name === 'saltLength')?.value as number; - } - if (!salt) { - salt = DEFAULT_PASSWORD_SALT_LENGTH; - } - context.parent[field.name] = await hashFunc(data, salt); - } - }, - }); - - await visitor.visit(model, action, args); - } -} diff --git a/packages/runtime/src/enhancements/node/policy/check-utils.ts b/packages/runtime/src/enhancements/node/policy/check-utils.ts deleted file mode 100644 index bb8a2c88e..000000000 --- a/packages/runtime/src/enhancements/node/policy/check-utils.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { match, P } from 'ts-pattern'; -import { ModelMeta, requireField } from '..'; -import type { DbClientContract } from '../../../types'; -import { createDeferredPromise } from '../promise'; -import { PermissionCheckArgs, PermissionCheckerConstraint } from '../types'; -import { prismaClientValidationError } from '../utils'; -import { ConstraintSolver } from './constraint-solver'; -import { PolicyUtil } from './policy-utils'; - -export async function checkPermission( - model: string, - args: PermissionCheckArgs, - modelMeta: ModelMeta, - policyUtils: PolicyUtil, - prisma: DbClientContract, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - prismaModule: any -) { - return createDeferredPromise(() => doCheckPermission(model, args, modelMeta, policyUtils, prisma, prismaModule)); -} - -async function doCheckPermission( - model: string, - args: PermissionCheckArgs, - modelMeta: ModelMeta, - policyUtils: PolicyUtil, - prisma: DbClientContract, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - prismaModule: any -) { - if (!['create', 'read', 'update', 'delete'].includes(args.operation)) { - throw prismaClientValidationError(prisma, prismaModule, `Invalid "operation" ${args.operation}`); - } - - let constraint = policyUtils.getCheckerConstraint(model, args.operation); - if (typeof constraint === 'boolean') { - return constraint; - } - - if (args.where) { - // combine runtime filters with generated constraints - - const extraConstraints: PermissionCheckerConstraint[] = []; - for (const [field, value] of Object.entries(args.where)) { - if (value === undefined) { - continue; - } - - if (value === null) { - throw prismaClientValidationError( - prisma, - prismaModule, - `Using "null" as filter value is not supported yet` - ); - } - - const fieldInfo = requireField(modelMeta, model, field); - - // relation and array fields are not supported - if (fieldInfo.isDataModel || fieldInfo.isArray) { - throw prismaClientValidationError( - prisma, - prismaModule, - `Providing filter for field "${field}" is not supported. Only scalar fields are allowed.` - ); - } - - // map field type to constraint type - const fieldType = match(fieldInfo.type) - .with(P.union('Int', 'BigInt', 'Float', 'Decimal'), () => 'number') - .with('String', () => 'string') - .with('Boolean', () => 'boolean') - .otherwise(() => { - throw prismaClientValidationError( - prisma, - prismaModule, - `Providing filter for field "${field}" is not supported. Only number, string, and boolean fields are allowed.` - ); - }); - - // check value type - const valueType = typeof value; - if (valueType !== 'number' && valueType !== 'string' && valueType !== 'boolean') { - throw prismaClientValidationError( - prisma, - prismaModule, - `Invalid value type for field "${field}". Only number, string or boolean is allowed.` - ); - } - - if (fieldType !== valueType) { - throw prismaClientValidationError( - prisma, - prismaModule, - `Invalid value type for field "${field}". Expected "${fieldType}".` - ); - } - - // check number validity - if (typeof value === 'number' && (!Number.isInteger(value) || value < 0)) { - throw prismaClientValidationError( - prisma, - prismaModule, - `Invalid value for field "${field}". Only non-negative integers are allowed.` - ); - } - - // build a constraint - extraConstraints.push({ - kind: 'eq', - left: { kind: 'variable', name: field, type: fieldType }, - right: { kind: 'value', value, type: fieldType }, - }); - } - - if (extraConstraints.length > 0) { - // combine the constraints - constraint = { kind: 'and', children: [constraint, ...extraConstraints] }; - } - } - - // check satisfiability - return new ConstraintSolver().checkSat(constraint); -} diff --git a/packages/runtime/src/enhancements/node/policy/constraint-solver.ts b/packages/runtime/src/enhancements/node/policy/constraint-solver.ts deleted file mode 100644 index 9b792a0fa..000000000 --- a/packages/runtime/src/enhancements/node/policy/constraint-solver.ts +++ /dev/null @@ -1,219 +0,0 @@ -import Logic from 'logic-solver'; -import { match } from 'ts-pattern'; -import type { - ComparisonConstraint, - ComparisonTerm, - LogicalConstraint, - PermissionCheckerConstraint, - ValueConstraint, - VariableConstraint, -} from '../types'; - -/** - * A boolean constraint solver based on `logic-solver`. Only boolean and integer types are supported. - */ -export class ConstraintSolver { - // a table for internalizing string literals - private stringTable: string[] = []; - - // a map for storing variable names and their corresponding formulas - private variables: Map = new Map(); - - /** - * Check the satisfiability of the given constraint. - */ - checkSat(constraint: PermissionCheckerConstraint): boolean { - // reset state - this.stringTable = []; - this.variables = new Map(); - - // convert the constraint to a "logic-solver" formula - const formula = this.buildFormula(constraint); - - // solve the formula - const solver = new Logic.Solver(); - solver.require(formula); - - // DEBUG: - // const solution = solver.solve(); - // if (solution) { - // console.log('Solution:'); - // this.variables.forEach((v, k) => console.log(`\t${k}=${solution?.evaluate(v)}`)); - // } else { - // console.log('No solution'); - // } - - return !!solver.solve(); - } - - private buildFormula(constraint: PermissionCheckerConstraint): Logic.Formula { - return match(constraint) - .when( - (c): c is ValueConstraint => c.kind === 'value', - (c) => this.buildValueFormula(c) - ) - .when( - (c): c is VariableConstraint => c.kind === 'variable', - (c) => this.buildVariableFormula(c) - ) - .when( - (c): c is ComparisonConstraint => ['eq', 'ne', 'gt', 'gte', 'lt', 'lte'].includes(c.kind), - (c) => this.buildComparisonFormula(c) - ) - .when( - (c): c is LogicalConstraint => ['and', 'or', 'not'].includes(c.kind), - (c) => this.buildLogicalFormula(c) - ) - .otherwise(() => { - throw new Error(`Unsupported constraint format: ${JSON.stringify(constraint)}`); - }); - } - - private buildLogicalFormula(constraint: LogicalConstraint) { - return match(constraint.kind) - .with('and', () => this.buildAndFormula(constraint)) - .with('or', () => this.buildOrFormula(constraint)) - .with('not', () => this.buildNotFormula(constraint)) - .exhaustive(); - } - - private buildAndFormula(constraint: LogicalConstraint): Logic.Formula { - if (constraint.children.some((c) => this.isFalse(c))) { - // short-circuit - return Logic.FALSE; - } - return Logic.and(...constraint.children.map((c) => this.buildFormula(c))); - } - - private buildOrFormula(constraint: LogicalConstraint): Logic.Formula { - if (constraint.children.some((c) => this.isTrue(c))) { - // short-circuit - return Logic.TRUE; - } - return Logic.or(...constraint.children.map((c) => this.buildFormula(c))); - } - - private buildNotFormula(constraint: LogicalConstraint) { - if (constraint.children.length !== 1) { - throw new Error('"not" constraint must have exactly one child'); - } - return Logic.not(this.buildFormula(constraint.children[0])); - } - - private isTrue(constraint: PermissionCheckerConstraint): unknown { - return constraint.kind === 'value' && constraint.value === true; - } - - private isFalse(constraint: PermissionCheckerConstraint): unknown { - return constraint.kind === 'value' && constraint.value === false; - } - - private buildComparisonFormula(constraint: ComparisonConstraint) { - if (constraint.left.kind === 'value' && constraint.right.kind === 'value') { - // constant comparison - const left: ValueConstraint = constraint.left; - const right: ValueConstraint = constraint.right; - return match(constraint.kind) - .with('eq', () => (left.value === right.value ? Logic.TRUE : Logic.FALSE)) - .with('ne', () => (left.value !== right.value ? Logic.TRUE : Logic.FALSE)) - .with('gt', () => (left.value > right.value ? Logic.TRUE : Logic.FALSE)) - .with('gte', () => (left.value >= right.value ? Logic.TRUE : Logic.FALSE)) - .with('lt', () => (left.value < right.value ? Logic.TRUE : Logic.FALSE)) - .with('lte', () => (left.value <= right.value ? Logic.TRUE : Logic.FALSE)) - .exhaustive(); - } - - return match(constraint.kind) - .with('eq', () => this.transformEquality(constraint.left, constraint.right)) - .with('ne', () => this.transformInequality(constraint.left, constraint.right)) - .with('gt', () => - this.transformComparison(constraint.left, constraint.right, (l, r) => Logic.greaterThan(l, r)) - ) - .with('gte', () => - this.transformComparison(constraint.left, constraint.right, (l, r) => Logic.greaterThanOrEqual(l, r)) - ) - .with('lt', () => - this.transformComparison(constraint.left, constraint.right, (l, r) => Logic.lessThan(l, r)) - ) - .with('lte', () => - this.transformComparison(constraint.left, constraint.right, (l, r) => Logic.lessThanOrEqual(l, r)) - ) - .exhaustive(); - } - - private buildVariableFormula(constraint: VariableConstraint) { - return ( - match(constraint.type) - .with('boolean', () => this.booleanVariable(constraint.name)) - .with('number', () => this.intVariable(constraint.name)) - // strings are internalized and represented by their indices - .with('string', () => this.intVariable(constraint.name)) - .exhaustive() - ); - } - - private buildValueFormula(constraint: ValueConstraint) { - return match(constraint.value) - .when( - (v): v is boolean => typeof v === 'boolean', - (v) => (v === true ? Logic.TRUE : Logic.FALSE) - ) - .when( - (v): v is number => typeof v === 'number', - (v) => Logic.constantBits(v) - ) - .when( - (v): v is string => typeof v === 'string', - (v) => { - // internalize the string and use its index as formula representation - const index = this.stringTable.indexOf(v); - if (index === -1) { - this.stringTable.push(v); - return Logic.constantBits(this.stringTable.length - 1); - } else { - return Logic.constantBits(index); - } - } - ) - .exhaustive(); - } - - private booleanVariable(name: string) { - this.variables.set(name, name); - return name; - } - - private intVariable(name: string) { - const r = Logic.variableBits(name, 32); - this.variables.set(name, r); - return r; - } - - private transformEquality(left: ComparisonTerm, right: ComparisonTerm) { - if (left.type !== right.type) { - throw new Error(`Type mismatch in equality constraint: ${JSON.stringify(left)}, ${JSON.stringify(right)}`); - } - - const leftFormula = this.buildFormula(left); - const rightFormula = this.buildFormula(right); - if (left.type === 'boolean' && right.type === 'boolean') { - // logical equivalence - return Logic.equiv(leftFormula, rightFormula); - } else { - // integer equality - return Logic.equalBits(leftFormula, rightFormula); - } - } - - private transformInequality(left: ComparisonTerm, right: ComparisonTerm) { - return Logic.not(this.transformEquality(left, right)); - } - - private transformComparison( - left: ComparisonTerm, - right: ComparisonTerm, - func: (left: Logic.Formula, right: Logic.Formula) => Logic.Formula - ) { - return func(this.buildFormula(left), this.buildFormula(right)); - } -} diff --git a/packages/runtime/src/enhancements/node/policy/handler.ts b/packages/runtime/src/enhancements/node/policy/handler.ts deleted file mode 100644 index 765d162b4..000000000 --- a/packages/runtime/src/enhancements/node/policy/handler.ts +++ /dev/null @@ -1,1837 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import deepmerge from 'deepmerge'; -import { CrudFailureReason } from '../../../constants'; -import { - ModelDataVisitor, - NestedWriteVisitor, - NestedWriteVisitorContext, - enumerate, - getIdFields, - getModelInfo, - requireField, - resolveField, - type FieldInfo, - type ModelMeta, -} from '../../../cross'; -import { getZodErrorMessage, invariant, lowerCaseFirst, upperCaseFirst } from '../../../local-helpers'; -import { EnhancementContext, PolicyOperationKind, type CrudContract, type DbClientContract } from '../../../types'; -import type { InternalEnhancementOptions } from '../create-enhancement'; -import { Logger } from '../logger'; -import { createDeferredPromise, createFluentPromise } from '../promise'; -import { PrismaProxyHandler } from '../proxy'; -import { QueryUtils } from '../query-utils'; -import type { EntityCheckerFunc, PermissionCheckArgs } from '../types'; -import { formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils'; -import { checkPermission } from './check-utils'; -import { PolicyUtil } from './policy-utils'; - -// a record for post-write policy check -type PostWriteCheckRecord = { - model: string; - operation: PolicyOperationKind; - uniqueFilter: any; - preValue?: any; -}; - -type FindOperations = 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany'; - -/** - * Prisma proxy handler for injecting access policy check. - */ -export class PolicyProxyHandler implements PrismaProxyHandler { - private readonly logger: Logger; - private readonly policyUtils: PolicyUtil; - private readonly model: string; - private readonly modelMeta: ModelMeta; - private readonly prismaModule: any; - private readonly queryUtils: QueryUtils; - - constructor( - private readonly prisma: DbClient, - model: string, - private readonly options: InternalEnhancementOptions, - private readonly context?: EnhancementContext - ) { - this.logger = new Logger(prisma); - this.model = lowerCaseFirst(model); - - ({ modelMeta: this.modelMeta, prismaModule: this.prismaModule } = options); - - this.policyUtils = new PolicyUtil(prisma, options, context, this.shouldLogQuery); - this.queryUtils = new QueryUtils(prisma, options); - } - - private get modelClient() { - return this.prisma[this.model]; - } - - //#region Find - - // find operations behaves as if the entities that don't match access policies don't exist - - findUnique(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.where) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'where field is required in query argument' - ); - } - - return this.findWithFluent('findUnique', args, () => null); - } - - findUniqueOrThrow(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.where) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'where field is required in query argument' - ); - } - - return this.findWithFluent('findUniqueOrThrow', args, () => { - throw this.policyUtils.notFound(this.model); - }); - } - - findFirst(args?: any) { - return this.findWithFluent('findFirst', args, () => null); - } - - findFirstOrThrow(args: any) { - return this.findWithFluent('findFirstOrThrow', args, () => { - throw this.policyUtils.notFound(this.model); - }); - } - - findMany(args?: any) { - return createDeferredPromise(() => this.doFind(args, 'findMany', () => [])); - } - - // make a find query promise with fluent API call stubs installed - private findWithFluent(method: FindOperations, args: any, handleRejection: () => any) { - args = this.policyUtils.safeClone(args); - return createFluentPromise( - () => this.doFind(args, method, handleRejection), - args, - this.options.modelMeta, - this.model - ); - } - - private async doFind(args: any, actionName: FindOperations, handleRejection: () => any) { - const origArgs = args; - const _args = this.policyUtils.safeClone(args); - if (!this.policyUtils.injectForRead(this.prisma, this.model, _args)) { - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`${actionName}\` ${this.model}: unconditionally denied`); - } - return handleRejection(); - } - - this.policyUtils.injectReadCheckSelect(this.model, _args); - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`${actionName}\` ${this.model}:\n${formatObject(_args)}`); - } - - const result = await this.modelClient[actionName](_args); - return this.policyUtils.postProcessForRead(result, this.model, origArgs); - } - - //#endregion - - //#region Create - - create(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'data field is required in query argument' - ); - } - - return createDeferredPromise(async () => { - this.policyUtils.tryReject(this.prisma, this.model, 'create'); - - const origArgs = args; - args = this.policyUtils.safeClone(args); - - // static input policy check for top-level create data - const inputCheck = this.policyUtils.checkInputGuard(this.model, args.data, 'create'); - if (inputCheck === false) { - throw this.policyUtils.deniedByPolicy( - this.model, - 'create', - undefined, - CrudFailureReason.ACCESS_POLICY_VIOLATION - ); - } - - const hasNestedCreateOrConnect = await this.hasNestedCreateOrConnect(args); - - const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { - if ( - // MUST check true here since inputCheck can be undefined (meaning static input check not possible) - inputCheck === true && - // simple create: no nested create/connect - !hasNestedCreateOrConnect - ) { - // there's no nested write and we've passed input check, proceed with the create directly - - // validate zod schema if any - args.data = this.validateCreateInputSchema(this.model, args.data); - - // make a create args only containing data and ID selection - const createArgs: any = { data: args.data, select: this.policyUtils.makeIdSelection(this.model) }; - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`create\` ${this.model}: ${formatObject(createArgs)}`); - } - const result = await tx[this.model].create(createArgs); - - // filter the read-back data - return this.policyUtils.readBack(tx, this.model, 'create', args, result); - } else { - // proceed with a complex create and collect post-write checks - const { result, postWriteChecks } = await this.doCreate(this.model, args, tx); - - // execute post-write checks - await this.runPostWriteChecks(postWriteChecks, tx); - - // filter the read-back data - return this.policyUtils.readBack(tx, this.model, 'create', origArgs, result); - } - }); - - if (error) { - throw error; - } else { - return result; - } - }); - } - - // create with nested write - private async doCreate(model: string, args: any, db: CrudContract) { - // record id fields involved in the nesting context - const idSelections: Array<{ path: FieldInfo[]; ids: string[] }> = []; - const pushIdFields = (model: string, context: NestedWriteVisitorContext) => { - const idFields = getIdFields(this.modelMeta, model); - idSelections.push({ - path: context.nestingPath.map((p) => p.field).filter((f): f is FieldInfo => !!f), - ids: idFields.map((f) => f.name), - }); - }; - - // create a string key that uniquely identifies an entity - const getEntityKey = (model: string, ids: any) => - `${upperCaseFirst(model)}#${Object.keys(ids) - .sort() - .map((f) => `${f}:${ids[f]?.toString()}`) - .join('_')}`; - - // record keys of entities that are connected instead of created - const connectedEntities = new Set(); - - // visit the create payload - const visitor = new NestedWriteVisitor(this.modelMeta, { - create: async (model, args, context) => { - const validateResult = this.validateCreateInputSchema(model, args); - if (validateResult !== args) { - this.policyUtils.replace(args, validateResult); - } - pushIdFields(model, context); - }, - - createMany: async (model, args, context) => { - enumerate(args.data).forEach((item) => { - const r = this.validateCreateInputSchema(model, item); - if (r !== item) { - this.policyUtils.replace(item, r); - } - }); - pushIdFields(model, context); - }, - - connectOrCreate: async (model, args, context) => { - if (!args.where) { - throw this.policyUtils.validationError(`'where' field is required for connectOrCreate`); - } - - if (args.create) { - args.create = this.validateCreateInputSchema(model, args.create); - } - - const existing = await this.policyUtils.checkExistence(db, model, args.where); - if (existing) { - // connect case - if (context.field?.backLink) { - const backLinkField = resolveField(this.modelMeta, model, context.field.backLink); - if (backLinkField?.isRelationOwner) { - // "connect" is actually "update" to foreign keys, so we need to map the "connect" payload - // to "update" payload by translating pk to fks, and use that to check update policies - const fieldsToUpdate = Object.values(backLinkField.foreignKeyMapping ?? {}); - await this.policyUtils.checkPolicyForUnique( - model, - args.where, - 'update', - db, - fieldsToUpdate - ); - } - } - - this.mergeToParent(context.parent, 'connect', args.where); - // record the key of connected entities so we can avoid validating them later - connectedEntities.add(getEntityKey(model, existing)); - } else { - // create case - pushIdFields(model, context); - - // create a new "create" clause at the parent level - this.mergeToParent(context.parent, 'create', args.create); - } - - // remove the connectOrCreate clause - this.removeFromParent(context.parent, 'connectOrCreate', args); - - // return false to prevent visiting the nested payload - return false; - }, - - connect: async (model, args, context) => { - if (!args || typeof args !== 'object' || Object.keys(args).length === 0) { - throw this.policyUtils.validationError(`'connect' field must be an non-empty object`); - } - - if (context.field?.backLink) { - const backLinkField = resolveField(this.modelMeta, model, context.field.backLink); - if (backLinkField?.isRelationOwner) { - // check existence - await this.policyUtils.checkExistence(db, model, args, true); - - // the target side of relation owns the relation, check if it's updatable - - // "connect" is actually "update" to foreign keys, so we need to map the "connect" payload - // to "update" payload by translating pk to fks, and use that to check update policies - const fieldsToUpdate = Object.values(backLinkField.foreignKeyMapping ?? {}); - await this.policyUtils.checkPolicyForUnique(model, args, 'update', db, fieldsToUpdate); - } - } - }, - }); - - await visitor.visit(model, 'create', args); - - // build the final "select" clause including all nested ID fields - let select: any = undefined; - if (idSelections.length > 0) { - select = {}; - idSelections.forEach(({ path, ids }) => { - let curr = select; - for (const p of path) { - if (!curr[p.name]) { - curr[p.name] = { select: {} }; - } - curr = curr[p.name].select; - } - Object.assign(curr, ...ids.map((f) => ({ [f]: true }))); - }); - } - - // proceed with the create - const createArgs = { data: args.data, select }; - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`create\` ${model}: ${formatObject(createArgs)}`); - } - const result = await db[model].create(createArgs); - - // post create policy check for the top-level and nested creates - const postCreateChecks = new Map(); - - // visit the create result and collect entities that need to be post-checked - const modelDataVisitor = new ModelDataVisitor(this.modelMeta); - modelDataVisitor.visit(model, result, (model, _data, scalarData) => { - const key = getEntityKey(model, scalarData); - // only check if entity is created, not connected - if (!connectedEntities.has(key) && !postCreateChecks.has(key)) { - const idFields = this.policyUtils.getIdFieldValues(model, scalarData); - postCreateChecks.set(key, { model, operation: 'create', uniqueFilter: idFields }); - } - }); - - // return only the ids of the top-level entity - const ids = this.policyUtils.getEntityIds(model, result); - return { result: ids, postWriteChecks: [...postCreateChecks.values()] }; - } - - // Checks if the given create payload has nested create or connect - private async hasNestedCreateOrConnect(args: any) { - let hasNestedCreateOrConnect = false; - - const visitor = new NestedWriteVisitor(this.modelMeta, { - async create(_model, _args, context) { - if (context.field) { - hasNestedCreateOrConnect = true; - return false; - } else { - return true; - } - }, - async connect() { - hasNestedCreateOrConnect = true; - return false; - }, - async connectOrCreate() { - hasNestedCreateOrConnect = true; - return false; - }, - async createMany() { - hasNestedCreateOrConnect = true; - return false; - }, - }); - - await visitor.visit(this.model, 'create', args); - return hasNestedCreateOrConnect; - } - - // Validates the given create payload against Zod schema if any - private validateCreateInputSchema(model: string, data: any) { - if (!data) { - return data; - } - - return this.policyUtils.validateZodSchema(model, 'create', data, false, (err) => { - throw this.policyUtils.deniedByPolicy( - model, - 'create', - `input failed validation: ${getZodErrorMessage(err)}`, - CrudFailureReason.DATA_VALIDATION_VIOLATION, - err - ); - }); - } - - createMany(args: { data: any; skipDuplicates?: boolean }) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'data field is required in query argument' - ); - } - - return createDeferredPromise(async () => { - this.policyUtils.tryReject(this.prisma, this.model, 'create'); - - args = this.policyUtils.safeClone(args); - - // `createManyAndReturn` may need to be converted to regular `create`s - const shouldConvertToCreate = this.preprocessCreateManyPayload(args); - - if (!shouldConvertToCreate) { - // direct `createMany` - return this.modelClient.createMany(args); - } else { - // create entities in a transaction with post-create checks - return this.queryUtils.transaction(this.prisma, async (tx) => { - const { result, postWriteChecks } = await this.doCreateMany(this.model, args, tx, 'createMany'); - // post-create check - await this.runPostWriteChecks(postWriteChecks, tx); - return { count: result.length }; - }); - } - }); - } - - createManyAndReturn(args: { data: any; select?: any; skipDuplicates?: boolean }) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'data field is required in query argument' - ); - } - - return createDeferredPromise(async () => { - this.policyUtils.tryReject(this.prisma, this.model, 'create'); - - const origArgs = args; - args = this.policyUtils.safeClone(args); - - // `createManyAndReturn` may need to be converted to regular `create`s - const shouldConvertToCreate = this.preprocessCreateManyPayload(args); - - let result: { result: unknown; error?: Error }[]; - - if (!shouldConvertToCreate) { - // direct `createManyAndReturn`, make sure we only select id fields for return - // so we can use the results directly for read-back check - const updatedArgs = { - ...args, - select: this.policyUtils.makeIdSelection(this.model), - include: undefined, - }; - const created = await this.modelClient.createManyAndReturn(updatedArgs); - - // process read-back - result = await Promise.all( - created.map((item) => this.policyUtils.readBack(this.prisma, this.model, 'create', origArgs, item)) - ); - } else { - // create entities in a transaction with post-create checks - result = await this.queryUtils.transaction(this.prisma, async (tx) => { - const { result: created, postWriteChecks } = await this.doCreateMany( - this.model, - args, - tx, - 'createManyAndReturn' - ); - - // post-create check - await this.runPostWriteChecks(postWriteChecks, tx); - - // process read-back - return Promise.all( - created.map((item) => this.policyUtils.readBack(tx, this.model, 'create', origArgs, item)) - ); - }); - } - - // throw read-back error if any of the create result read-back fails - const error = result.find((r) => !!r.error)?.error; - if (error) { - throw error; - } else { - return result.map((r) => r.result); - } - }); - } - - /** - * Preprocess the payload of `createMany` and `createManyAndReturn` and update in place if needed. - * @returns `true` if the operation should be converted to regular `create`s; false otherwise. - */ - private preprocessCreateManyPayload(args: { data: any; select?: any; skipDuplicates?: boolean }) { - if (!args) { - return false; - } - - // if post-create check is needed - const needPostCreateCheck = this.validateCreateInput(args); - - // if the payload has any relation fields. Note that other enhancements (`withDefaultInAuth` for now) - // can introduce relation fields into the payload - let hasRelationFields = false; - if (args.data) { - hasRelationFields = this.hasRelationFieldsInPayload(this.model, args.data); - } - - return needPostCreateCheck || hasRelationFields; - } - - private hasRelationFieldsInPayload(model: string, payload: any) { - const modelInfo = getModelInfo(this.modelMeta, model); - if (!modelInfo) { - return false; - } - - for (const item of enumerate(payload)) { - for (const field of Object.keys(item)) { - const fieldInfo = resolveField(this.modelMeta, model, field); - if (fieldInfo?.isDataModel) { - return true; - } - } - } - - return false; - } - - private validateCreateInput(args: { data: any; skipDuplicates?: boolean | undefined }) { - let needPostCreateCheck = false; - for (const item of enumerate(args.data)) { - const validationResult = this.validateCreateInputSchema(this.model, item); - if (validationResult !== item) { - this.policyUtils.replace(item, validationResult); - } - - const inputCheck = this.policyUtils.checkInputGuard(this.model, item, 'create'); - if (inputCheck === false) { - // unconditionally deny - throw this.policyUtils.deniedByPolicy( - this.model, - 'create', - undefined, - CrudFailureReason.ACCESS_POLICY_VIOLATION - ); - } else if (inputCheck === true) { - // unconditionally allow - } else if (inputCheck === undefined) { - // static policy check is not possible, need to do post-create check - needPostCreateCheck = true; - } - } - return needPostCreateCheck; - } - - private async doCreateMany( - model: string, - args: { data: any; skipDuplicates?: boolean }, - db: CrudContract, - action: 'createMany' | 'createManyAndReturn' - ) { - // We can't call the native "createMany" because we can't get back what was created - // for post-create checks. Instead, do a "create" for each item and collect the results. - - let createResult = await Promise.all( - enumerate(args.data).map(async (item) => { - if (args.skipDuplicates) { - if (await this.hasDuplicatedUniqueConstraint(model, item, undefined, db)) { - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`createMany\` skipping duplicate ${formatObject(item)}`); - } - return undefined; - } - } - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`create\` for \`${action}\` ${model}: ${formatObject(item)}`); - } - return await db[model].create({ select: this.policyUtils.makeIdSelection(model), data: item }); - }) - ); - - // filter undefined values due to skipDuplicates - createResult = createResult.filter((p) => !!p); - - return { - result: createResult, - postWriteChecks: createResult.map((item) => ({ - model, - operation: 'create' as PolicyOperationKind, - uniqueFilter: item, - })), - }; - } - - private async hasDuplicatedUniqueConstraint(model: string, createData: any, upstreamQuery: any, db: CrudContract) { - // check unique constraint conflicts - // we can't rely on try/catch/ignore constraint violation error: https://github.com/prisma/prisma/issues/20496 - // TODO: for simple cases we should be able to translate it to an `upsert` with empty `update` payload - - // for each unique constraint, check if the input item has all fields set, and if so, check if - // an entity already exists, and ignore accordingly - - const uniqueConstraints = this.policyUtils.getUniqueConstraints(model); - - for (const constraint of Object.values(uniqueConstraints)) { - // the unique filter used to check existence - const uniqueFilter: any = {}; - - // unique constraint fields not covered yet - const remainingConstraintFields = new Set(constraint.fields); - - // collect constraint fields from the create data - for (const [k, v] of Object.entries(createData)) { - if (v === undefined) { - continue; - } - - if (remainingConstraintFields.has(k)) { - uniqueFilter[k] = v; - remainingConstraintFields.delete(k); - } - } - - // collect constraint fields from the upstream query - if (upstreamQuery) { - for (const [k, v] of Object.entries(upstreamQuery)) { - if (v === undefined) { - continue; - } - - if (remainingConstraintFields.has(k)) { - uniqueFilter[k] = v; - remainingConstraintFields.delete(k); - continue; - } - - // check if the upstream query contains a relation field which covers - // a foreign key field constraint - - const fieldInfo = requireField(this.modelMeta, model, k); - if (!fieldInfo.isDataModel) { - // only care about relation fields - continue; - } - - // merge the upstream query into the unique filter - uniqueFilter[k] = v; - - // mark the corresponding foreign key fields as covered - const fkMapping = fieldInfo.foreignKeyMapping ?? {}; - for (const fk of Object.values(fkMapping)) { - remainingConstraintFields.delete(fk); - } - } - } - - if (remainingConstraintFields.size === 0) { - // all constraint fields set, check existence - const existing = await this.policyUtils.checkExistence(db, model, uniqueFilter); - if (existing) { - return true; - } - } - } - - return false; - } - - //#endregion - - //#region Update & Upsert - - // "update" and "upsert" work against unique entity, so we actively rejects the request if the - // entity fails policy check - // - // "updateMany" works against a set of entities, entities not passing policy check are silently - // ignored - - update(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.where) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'where field is required in query argument' - ); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'data field is required in query argument' - ); - } - - return createDeferredPromise(async () => { - args = this.policyUtils.safeClone(args); - - const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { - // proceed with nested writes and collect post-write checks - const { result, postWriteChecks } = await this.doUpdate(args, tx); - - // post-write check - await this.runPostWriteChecks(postWriteChecks, tx); - - // filter the read-back data - return this.policyUtils.readBack(tx, this.model, 'update', args, result); - }); - - if (error) { - throw error; - } else { - return result; - } - }); - } - - private async doUpdate(args: any, db: CrudContract) { - // collected post-update checks - const postWriteChecks: PostWriteCheckRecord[] = []; - - // registers a post-update check task - const _registerPostUpdateCheck = async ( - model: string, - preUpdateLookupFilter: any, - postUpdateLookupFilter: any - ) => { - // both "post-update" rules and Zod schemas require a post-update check - if (this.policyUtils.hasAuthGuard(model, 'postUpdate') || this.policyUtils.getZodSchema(model)) { - // select pre-update field values - let preValue: any; - const preValueSelect = this.policyUtils.getPreValueSelect(model); - if (preValueSelect && Object.keys(preValueSelect).length > 0) { - preValue = await db[model].findFirst({ where: preUpdateLookupFilter, select: preValueSelect }); - } - postWriteChecks.push({ - model, - operation: 'postUpdate', - uniqueFilter: postUpdateLookupFilter, - preValue, - }); - } - }; - - // We can't let the native "update" to handle nested "create" because we can't get back what - // was created for doing post-update checks. - // Instead, handle nested create inside update as an atomic operation that creates an entire - // subtree (containing nested creates/connects) - - const _create = async (model: string, args: any, context: NestedWriteVisitorContext) => { - let createData = args; - if (context.field?.backLink) { - // Check if the create payload contains any "unsafe" assignment: - // assign id or foreign key fields. - // - // The reason why we need to do that is Prisma's mutations payload - // structure has two mutually exclusive forms for safe and unsafe - // operations. E.g.: - // - safe: { data: { user: { connect: { id: 1 }} } } - // - unsafe: { data: { userId: 1 } } - const unsafe = isUnsafeMutate(model, args, this.modelMeta); - - // handles the connection to upstream entity - const reversedQuery = await this.policyUtils.buildReversedQuery(db, context, true, unsafe); - if ((!unsafe || context.field.isRelationOwner) && reversedQuery[context.field.backLink]) { - // if mutation is safe, or current field owns the relation (so the other side has no fk), - // and the reverse query contains the back link, then we can build a "connect" with it - createData = { - ...createData, - [context.field.backLink]: { - connect: reversedQuery[context.field.backLink], - }, - }; - } else { - // otherwise, the reverse query should be translated to foreign key setting - // and merged to the create data - - const backLinkField = this.requireBackLink(context.field); - invariant(backLinkField.foreignKeyMapping); - - // try to extract foreign key values from the reverse query - let fkValues = Object.values(backLinkField.foreignKeyMapping).reduce((obj, fk) => { - obj[fk] = reversedQuery[fk]; - return obj; - }, {}); - - if (Object.values(fkValues).every((v) => v !== undefined)) { - // all foreign key values are available, merge them to the create data - createData = { - ...createData, - ...fkValues, - }; - } else { - // some foreign key values are missing, need to look up the upstream entity, - // this can happen when the upstream entity doesn't have a unique where clause, - // for example when it's nested inside a one-to-one update - const upstreamQuery = { - where: reversedQuery[backLinkField.name], - select: this.policyUtils.makeIdSelection(backLinkField.type), - }; - - // fetch the upstream entity - if (this.shouldLogQuery) { - this.logger.info( - `[policy] \`findUniqueOrThrow\` ${model}: looking up upstream entity of ${ - backLinkField.type - }, ${formatObject(upstreamQuery)}` - ); - } - const upstreamEntity = await this.prisma[backLinkField.type].findUniqueOrThrow(upstreamQuery); - - // map ids to foreign keys - fkValues = Object.entries(backLinkField.foreignKeyMapping).reduce((obj, [id, fk]) => { - obj[fk] = upstreamEntity[id]; - return obj; - }, {}); - - // merge them to the create data - createData = { ...createData, ...fkValues }; - } - } - } - - // proceed with the create and collect post-create checks - const { postWriteChecks: checks, result } = await this.doCreate(model, { data: createData }, db); - postWriteChecks.push(...checks); - - return result; - }; - - const _createMany = async ( - model: string, - args: { data: any; skipDuplicates?: boolean }, - context: NestedWriteVisitorContext - ) => { - for (const item of enumerate(args.data)) { - if (args.skipDuplicates) { - // get a reversed query to include fields inherited from upstream mutation, - // it'll be merged with the create payload for unique constraint checking - const upstreamQuery = await this.policyUtils.buildReversedQuery(db, context); - if (await this.hasDuplicatedUniqueConstraint(model, item, upstreamQuery, db)) { - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`createMany\` skipping duplicate ${formatObject(item)}`); - } - continue; - } - } - await _create(model, item, context); - } - }; - - const _connectDisconnect = async ( - model: string, - args: any, - context: NestedWriteVisitorContext, - operation: 'connect' | 'disconnect' - ) => { - if (context.field?.backLink) { - const backLinkField = this.policyUtils.getModelField(model, context.field.backLink); - if (backLinkField?.isRelationOwner) { - let uniqueFilter = args; - if (operation === 'disconnect') { - // disconnect filter is not unique, need to build a reversed query to - // locate the entity and use its id fields as unique filter - const reversedQuery = await this.policyUtils.buildReversedQuery(db, context); - const found = await db[model].findUnique({ - where: reversedQuery, - select: this.policyUtils.makeIdSelection(model), - }); - uniqueFilter = found && this.policyUtils.getIdFieldValues(model, found); - } - - // update happens on the related model, require updatable, - // `uniqueFilter` can be undefined if the entity to be disconnected doesn't exist - if (uniqueFilter) { - // check for update, "connect" and "disconnect" are actually "update" to foreign keys - const fieldsToUpdate = Object.values(backLinkField.foreignKeyMapping ?? {}); - await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'update', db, fieldsToUpdate); - - // register post-update check - await _registerPostUpdateCheck(model, uniqueFilter, uniqueFilter); - } - } - } - }; - - // visit nested writes - const visitor = new NestedWriteVisitor(this.modelMeta, { - update: async (model, args, context) => { - // build a unique query including upstream conditions - const uniqueFilter = await this.policyUtils.buildReversedQuery(db, context); - - // handle not-found - const existing = await this.policyUtils.checkExistence(db, model, uniqueFilter, true); - - // check if the update actually writes to this model - let thisModelUpdate = false; - const updatePayload = (args as any).data ?? args; - - const validatedPayload = this.validateUpdateInputSchema(model, updatePayload); - if (validatedPayload !== updatePayload) { - this.policyUtils.replace(updatePayload, validatedPayload); - } - - if (updatePayload) { - for (const key of Object.keys(updatePayload)) { - const field = resolveField(this.modelMeta, model, key); - if (field) { - if (!field.isDataModel) { - // scalar field, require this model to be updatable - thisModelUpdate = true; - break; - } else if (field.isRelationOwner) { - // relation is being updated and this model owns foreign key, require updatable - thisModelUpdate = true; - break; - } - } - } - } - - if (thisModelUpdate) { - this.policyUtils.tryReject(db, this.model, 'update'); - - // check pre-update guard - await this.policyUtils.checkPolicyForUnique( - model, - uniqueFilter, - 'update', - db, - this.queryUtils.getFieldsWithDefinedValues(updatePayload) - ); - - // handle the case where id fields are updated - const _args: any = args; - const checkPayload = _args.data && typeof _args.data === 'object' ? _args.data : _args; - const postUpdateIds = this.calculatePostUpdateIds(model, existing, checkPayload); - - // register post-update check - await _registerPostUpdateCheck(model, existing, postUpdateIds); - } - }, - - updateMany: async (model, args, context) => { - // prepare for post-update check - if (this.policyUtils.hasAuthGuard(model, 'postUpdate') || this.policyUtils.getZodSchema(model)) { - let select = this.policyUtils.makeIdSelection(model); - const preValueSelect = this.policyUtils.getPreValueSelect(model); - if (preValueSelect) { - select = { ...select, ...preValueSelect }; - } - const reversedQuery = await this.policyUtils.buildReversedQuery(db, context); - const currentSetQuery = { select, where: reversedQuery }; - this.policyUtils.injectAuthGuardAsWhere(db, currentSetQuery, model, 'read'); - - if (this.shouldLogQuery) { - this.logger.info( - `[policy] \`findMany\` for post update check ${model}:\n${formatObject(currentSetQuery)}` - ); - } - const currentSet = await db[model].findMany(currentSetQuery); - - postWriteChecks.push( - ...currentSet.map((preValue) => ({ - model, - operation: 'postUpdate' as PolicyOperationKind, - uniqueFilter: preValue, - preValue: preValueSelect ? preValue : undefined, - })) - ); - } - - args.data = this.validateUpdateInputSchema(model, args.data); - - const updateGuard = this.policyUtils.getAuthGuard(db, model, 'update'); - if (this.policyUtils.isTrue(updateGuard) || this.policyUtils.isFalse(updateGuard)) { - // injects simple auth guard into where clause - this.policyUtils.injectAuthGuardAsWhere(db, args, model, 'update'); - } else { - // we have to process `updateMany` separately because the guard may contain - // filters using relation fields which are not allowed in nested `updateMany` - const reversedQuery = await this.policyUtils.buildReversedQuery(db, context); - const updateWhere = this.policyUtils.and(reversedQuery, updateGuard); - if (this.shouldLogQuery) { - this.logger.info( - `[policy] \`updateMany\` ${model}:\n${formatObject({ - where: updateWhere, - data: args.data, - })}` - ); - } - await db[model].updateMany({ where: updateWhere, data: args.data }); - delete context.parent.updateMany; - } - }, - - create: async (model, args, context) => { - // process the entire create subtree separately - await _create(model, args, context); - - // remove it from the update payload - this.removeFromParent(context.parent, 'create', args); - - // don't visit payload - return false; - }, - - createMany: async (model, args, context) => { - // process createMany separately - await _createMany(model, args, context); - - // remove it from the update payload - delete context.parent.createMany; - - // don't visit payload - return false; - }, - - upsert: async (model, args, context) => { - // build a unique query including upstream conditions - const uniqueFilter = await this.policyUtils.buildReversedQuery(db, context); - - // branch based on if the update target exists - const existing = await this.policyUtils.checkExistence(db, model, uniqueFilter); - if (existing) { - // update case - - // check pre-update guard - await this.policyUtils.checkPolicyForUnique( - model, - existing, - 'update', - db, - this.queryUtils.getFieldsWithDefinedValues(args.update) - ); - - // handle the case where id fields are updated - const postUpdateIds = this.calculatePostUpdateIds(model, existing, args.update); - - // register post-update check - await _registerPostUpdateCheck(model, existing, postUpdateIds); - - // convert upsert to update - const convertedUpdate = { - where: args.where ?? {}, - data: this.validateUpdateInputSchema(model, args.update), - }; - this.mergeToParent(context.parent, 'update', convertedUpdate); - this.removeFromParent(context.parent, 'upsert', args); - - // continue visiting the new payload - return convertedUpdate; - } else { - // create case - - // process the entire create subtree separately - await _create(model, args.create, context); - - // remove it from the update payload - this.removeFromParent(context.parent, 'upsert', args); - - // don't visit payload - return false; - } - }, - - connect: async (model, args, context) => _connectDisconnect(model, args, context, 'connect'), - - connectOrCreate: async (model, args, context) => { - // the where condition is already unique, so we can use it to check if the target exists - const existing = await this.policyUtils.checkExistence(db, model, args.where); - if (existing) { - // connect - await _connectDisconnect(model, args.where, context, 'connect'); - return true; - } else { - // create - const created = await _create(model, args.create, context); - - const upperContext = context.nestingPath[context.nestingPath.length - 2]; - if (upperContext?.where && context.field) { - // check if the where clause of the upper context references the id - // of the connected entity, if so, we need to update it - this.overrideForeignKeyFields(upperContext.model, upperContext.where, context.field, created); - } - - // remove the payload from the parent - this.removeFromParent(context.parent, 'connectOrCreate', args); - - return false; - } - }, - - disconnect: async (model, args, context) => _connectDisconnect(model, args, context, 'disconnect'), - - set: async (model, args, context) => { - // find the set of items to be replaced - const reversedQuery = await this.policyUtils.buildReversedQuery(db, context); - const findCurrSetArgs = { - select: this.policyUtils.makeIdSelection(model), - where: reversedQuery, - }; - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`findMany\` ${model}:\n${formatObject(findCurrSetArgs)}`); - } - const currentSet = await db[model].findMany(findCurrSetArgs); - - // register current set for update (foreign key) - await Promise.all(currentSet.map((item) => _connectDisconnect(model, item, context, 'disconnect'))); - - // proceed with connecting the new set - await Promise.all(enumerate(args).map((item) => _connectDisconnect(model, item, context, 'connect'))); - }, - - delete: async (model, args, context) => { - // build a unique query including upstream conditions - const uniqueFilter = await this.policyUtils.buildReversedQuery(db, context); - - // handle not-found - await this.policyUtils.checkExistence(db, model, uniqueFilter, true); - - // check delete guard - await this.policyUtils.checkPolicyForUnique(model, uniqueFilter, 'delete', db, []); - }, - - deleteMany: async (model, args, context) => { - const guard = await this.policyUtils.getAuthGuard(db, model, 'delete'); - if (this.policyUtils.isTrue(guard) || this.policyUtils.isFalse(guard)) { - // inject simple auth guard - context.parent.deleteMany = this.policyUtils.and(args, guard); - } else { - // we have to process `deleteMany` separately because the guard may contain - // filters using relation fields which are not allowed in nested `deleteMany` - const reversedQuery = await this.policyUtils.buildReversedQuery(db, context); - const deleteWhere = this.policyUtils.and(reversedQuery, guard); - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`deleteMany\` ${model}:\n${formatObject({ where: deleteWhere })}`); - } - await db[model].deleteMany({ where: deleteWhere }); - delete context.parent.deleteMany; - } - }, - }); - - await visitor.visit(this.model, 'update', args); - - // finally proceed with the update - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`update\` ${this.model}: ${formatObject(args)}`); - } - const result = await db[this.model].update({ - where: args.where, - data: args.data, - select: this.policyUtils.makeIdSelection(this.model), - }); - - return { result, postWriteChecks }; - } - - // calculate id fields used for post-update check given an update payload - private calculatePostUpdateIds(_model: string, currentIds: any, updatePayload: any) { - const result = this.policyUtils.safeClone(currentIds); - for (const key of Object.keys(currentIds)) { - const updateValue = updatePayload[key]; - if (typeof updateValue === 'string' || typeof updateValue === 'number' || typeof updateValue === 'bigint') { - result[key] = updateValue; - } - } - return result; - } - - // updates foreign key fields inside `payload` based on relation id fields in `newIds` - private overrideForeignKeyFields( - model: string, - payload: any, - relation: FieldInfo, - newIds: Record - ) { - if (!relation.foreignKeyMapping || Object.keys(relation.foreignKeyMapping).length === 0) { - return; - } - - // override foreign key values - for (const [id, fk] of Object.entries(relation.foreignKeyMapping)) { - if (payload[fk] !== undefined && newIds[id] !== undefined) { - payload[fk] = newIds[id]; - } - } - - // deal with compound id fields - const uniqueConstraints = this.policyUtils.getUniqueConstraints(model); - for (const [name, constraint] of Object.entries(uniqueConstraints)) { - if (constraint.fields.length > 1) { - const target = payload[name]; - if (target) { - for (const [id, fk] of Object.entries(relation.foreignKeyMapping)) { - if (target[fk] !== undefined && newIds[id] !== undefined) { - target[fk] = newIds[id]; - } - } - } - } - } - } - - // Validates the given update payload against Zod schema if any - private validateUpdateInputSchema(model: string, data: any) { - if (!data) { - return data; - } - - // update payload can contain non-literal fields, like: - // { x: { increment: 1 } } - // we should only validate literal fields - const literalData = Object.entries(data).reduce( - (acc, [k, v]) => ({ ...acc, ...(typeof v !== 'object' ? { [k]: v } : {}) }), - {} - ); - - const validatedData: any = this.policyUtils.validateZodSchema(model, 'update', literalData, false, (err) => { - throw this.policyUtils.deniedByPolicy( - model, - 'update', - `input failed validation: ${getZodErrorMessage(err)}`, - CrudFailureReason.DATA_VALIDATION_VIOLATION, - err - ); - }); - - // schema may have transformed field values, use it to overwrite the original data - return { ...data, ...validatedData }; - } - - updateMany(args: any) { - return this.doUpdateMany(args, 'updateMany'); - } - - updateManyAndReturn(args: any): Promise { - return this.doUpdateMany(args, 'updateManyAndReturn'); - } - - private doUpdateMany(args: any, action: 'updateMany' | 'updateManyAndReturn'): Promise { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.data) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'data field is required in query argument' - ); - } - - return createDeferredPromise(async () => { - this.policyUtils.tryReject(this.prisma, this.model, 'update'); - - const origArgs = args; - args = this.policyUtils.safeClone(args); - this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update'); - - args.data = this.validateUpdateInputSchema(this.model, args.data); - - const entityChecker = this.policyUtils.getEntityChecker(this.model, 'update'); - - const canProceedWithoutTransaction = - // no post-update rules - !this.policyUtils.hasAuthGuard(this.model, 'postUpdate') && - // no Zod schema - !this.policyUtils.getZodSchema(this.model) && - // no entity checker - !entityChecker; - - if (canProceedWithoutTransaction) { - // proceed without a transaction - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`updateMany\` ${this.model}: ${formatObject(args)}`); - } - if (action === 'updateMany') { - return this.modelClient.updateMany(args); - } else { - // make sure only id fields are returned so we can directly use the result - // for read-back check - const updatedArg = { - ...args, - select: this.policyUtils.makeIdSelection(this.model), - include: undefined, - }; - const updated = await this.modelClient.updateManyAndReturn(updatedArg); - // process read-back - const result = await Promise.all( - updated.map((item) => - this.policyUtils.readBack(this.prisma, this.model, 'update', origArgs, item) - ) - ); - // throw read-back error if any of create result read-back fails - const error = result.find((r) => !!r.error)?.error; - if (error) { - throw error; - } else { - return result.map((r) => r.result); - } - } - } - - // collect post-update checks - const postWriteChecks: PostWriteCheckRecord[] = []; - - const result = await this.queryUtils.transaction(this.prisma, async (tx) => { - // collect pre-update values - let select = this.policyUtils.makeIdSelection(this.model); - const preValueSelect = this.policyUtils.getPreValueSelect(this.model); - if (preValueSelect) { - select = { ...select, ...preValueSelect }; - } - - // merge selection required for running additional checker - const entityChecker = this.policyUtils.getEntityChecker(this.model, 'update'); - if (entityChecker?.selector) { - select = deepmerge(select, entityChecker.selector); - } - - const currentSetQuery = { select, where: args.where }; - this.policyUtils.injectAuthGuardAsWhere(tx, currentSetQuery, this.model, 'update'); - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`findMany\` ${this.model}: ${formatObject(currentSetQuery)}`); - } - let candidates = await tx[this.model].findMany(currentSetQuery); - - if (entityChecker) { - // filter candidates with additional checker and build an id filter - const r = this.buildIdFilterWithEntityChecker(candidates, entityChecker.func); - candidates = r.filteredCandidates; - - // merge id filter into update's where clause - args.where = args.where ? { AND: [args.where, r.idFilter] } : r.idFilter; - } - - postWriteChecks.push( - ...candidates.map((preValue) => ({ - model: this.model, - operation: 'postUpdate' as PolicyOperationKind, - uniqueFilter: this.policyUtils.getEntityIds(this.model, preValue), - preValue: preValueSelect ? preValue : undefined, - })) - ); - - // proceed with the update - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`updateMany\` in tx for ${this.model}: ${formatObject(args)}`); - } - - if (action === 'updateMany') { - const result = await tx[this.model].updateMany(args); - // run post-write checks - await this.runPostWriteChecks(postWriteChecks, tx); - return result; - } else { - // make sure only id fields are returned so we can directly use the result - // for read-back check - const updatedArg = { - ...args, - select: this.policyUtils.makeIdSelection(this.model), - include: undefined, - }; - const result = await tx[this.model].updateManyAndReturn(updatedArg); - // run post-write checks - await this.runPostWriteChecks(postWriteChecks, tx); - return result; - } - }); - - if (action === 'updateMany') { - // no further processing needed - return result; - } else { - // process read-back - const readBackResult = await Promise.all( - (result as unknown[]).map((item) => - this.policyUtils.readBack(this.prisma, this.model, 'update', origArgs, item) - ) - ); - // throw read-back error if any of the update result read-back fails - const error = readBackResult.find((r) => !!r.error)?.error; - if (error) { - throw error; - } else { - return readBackResult.map((r) => r.result); - } - } - }); - } - - upsert(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.where) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'where field is required in query argument' - ); - } - if (!args.create) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'create field is required in query argument' - ); - } - if (!args.update) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'update field is required in query argument' - ); - } - - return createDeferredPromise(async () => { - this.policyUtils.tryReject(this.prisma, this.model, 'create'); - this.policyUtils.tryReject(this.prisma, this.model, 'update'); - - args = this.policyUtils.safeClone(args); - - // We can call the native "upsert" because we can't tell if an entity was created or updated - // for doing post-write check accordingly. Instead, decompose it into create or update. - - const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { - const { where, create, update, ...rest } = args; - const existing = await this.policyUtils.checkExistence(tx, this.model, where); - - if (existing) { - // update case - const { result, postWriteChecks } = await this.doUpdate( - { - where: this.policyUtils.composeCompoundUniqueField(this.model, existing), - data: update, - ...rest, - }, - tx - ); - await this.runPostWriteChecks(postWriteChecks, tx); - return this.policyUtils.readBack(tx, this.model, 'update', args, result); - } else { - // create case - const { result, postWriteChecks } = await this.doCreate(this.model, { data: create, ...rest }, tx); - await this.runPostWriteChecks(postWriteChecks, tx); - return this.policyUtils.readBack(tx, this.model, 'create', args, result); - } - }); - - if (error) { - throw error; - } else { - return result; - } - }); - } - - //#endregion - - //#region Delete - - // "delete" works against a single entity, and is rejected if the entity fails policy check. - // "deleteMany" works against a set of entities, entities that fail policy check are filtered out. - - delete(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - if (!args.where) { - throw prismaClientValidationError( - this.prisma, - this.prismaModule, - 'where field is required in query argument' - ); - } - - return createDeferredPromise(async () => { - this.policyUtils.tryReject(this.prisma, this.model, 'delete'); - - const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => { - // do a read-back before delete - const r = await this.policyUtils.readBack(tx, this.model, 'delete', args, args.where); - const error = r.error; - const read = r.result; - - // check existence - await this.policyUtils.checkExistence(tx, this.model, args.where, true); - - // inject delete guard - await this.policyUtils.checkPolicyForUnique(this.model, args.where, 'delete', tx, []); - - // proceed with the deletion - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`delete\` ${this.model}:\n${formatObject(args)}`); - } - await tx[this.model].delete(args); - - return { result: read, error }; - }); - - if (error) { - throw error; - } else { - return result; - } - }); - } - - deleteMany(args: any) { - return createDeferredPromise(() => { - this.policyUtils.tryReject(this.prisma, this.model, 'delete'); - - // inject policy conditions - args = this.policyUtils.safeClone(args); - this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'delete'); - - const entityChecker = this.policyUtils.getEntityChecker(this.model, 'delete'); - if (entityChecker) { - // additional checker exists, need to run deletion inside a transaction - return this.queryUtils.transaction(this.prisma, async (tx) => { - // find the delete candidates, selecting id fields and fields needed for - // running the additional checker - let candidateSelect = this.policyUtils.makeIdSelection(this.model); - if (entityChecker.selector) { - candidateSelect = deepmerge(candidateSelect, entityChecker.selector); - } - - if (this.shouldLogQuery) { - this.logger.info( - `[policy] \`findMany\` ${this.model}: ${formatObject({ - where: args.where ?? {}, - select: candidateSelect, - })}` - ); - } - const candidates = await tx[this.model].findMany({ - where: args.where ?? {}, - select: candidateSelect, - }); - - // build a ID filter based on id values filtered by the additional checker - const { idFilter } = this.buildIdFilterWithEntityChecker(candidates, entityChecker.func); - - // merge the ID filter into the where clause - args.where = args.where ? { AND: [args.where, idFilter] } : idFilter; - - // finally, conduct the deletion with the combined where clause - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`deleteMany\` in tx for ${this.model}:\n${formatObject(args)}`); - } - return tx[this.model].deleteMany(args); - }); - } else { - // conduct the deletion directly - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`deleteMany\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.deleteMany(args); - } - }); - } - - //#endregion - - //#region Aggregation - - aggregate(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - - return createDeferredPromise(() => { - args = this.policyUtils.safeClone(args); - - // inject policy conditions - this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`aggregate\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.aggregate(args); - }); - } - - groupBy(args: any) { - if (!args) { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'query argument is required'); - } - - return createDeferredPromise(() => { - args = this.policyUtils.safeClone(args); - - // inject policy conditions - this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`groupBy\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.groupBy(args); - }); - } - - count(args: any) { - return createDeferredPromise(() => { - // inject policy conditions - args = args ? this.policyUtils.safeClone(args) : {}; - this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`count\` ${this.model}:\n${formatObject(args)}`); - } - return this.modelClient.count(args); - }); - } - - //#endregion - - //#region Prisma Pulse - - subscribe(args: any) { - return this.handleSubscribeStream('subscribe', args); - } - - stream(args: any) { - return this.handleSubscribeStream('stream', args); - } - - private async handleSubscribeStream(action: 'subscribe' | 'stream', args: any) { - if (!args) { - // include all - args = { create: {}, update: {}, delete: {} }; - } else { - if (typeof args !== 'object') { - throw prismaClientValidationError(this.prisma, this.prismaModule, 'argument must be an object'); - } - args = this.policyUtils.safeClone(args); - } - - // inject read guard as subscription filter - for (const key of ['create', 'update', 'delete']) { - if (args[key] === undefined) { - continue; - } - // "update" has an extra layer of "after" - const payload = key === 'update' ? args[key].after : args[key]; - const toInject = { where: payload }; - this.policyUtils.injectForRead(this.prisma, this.model, toInject); - if (key === 'update') { - // "update" has an extra layer of "after" - args[key].after = toInject.where; - } else { - args[key] = toInject.where; - } - } - - if (this.shouldLogQuery) { - this.logger.info(`[policy] \`${action}\` ${this.model}:\n${formatObject(args)}`); - } - - // Prisma Pulse returns an async iterable, which we need to wrap - // and post-process the iteration results - const iterable = await this.modelClient[action](args); - return { - [Symbol.asyncIterator]: () => { - const iter = iterable[Symbol.asyncIterator].bind(iterable)(); - return { - next: async () => { - const { done, value } = await iter.next(); - let processedValue = value; - if (value && 'action' in value) { - switch (value.action) { - case 'create': - if ('created' in value) { - processedValue = { - ...value, - created: this.policyUtils.postProcessForRead(value.created, this.model, {}), - }; - } - break; - - case 'update': - if ('before' in value) { - processedValue = { - ...value, - before: this.policyUtils.postProcessForRead(value.before, this.model, {}), - }; - } - if ('after' in value) { - processedValue = { - ...value, - after: this.policyUtils.postProcessForRead(value.after, this.model, {}), - }; - } - break; - - case 'delete': - if ('deleted' in value) { - processedValue = { - ...value, - deleted: this.policyUtils.postProcessForRead(value.deleted, this.model, {}), - }; - } - break; - } - } - - return { done, value: processedValue }; - }, - return: () => iter.return?.(), - throw: () => iter.throw?.(), - }; - }, - }; - } - - //#endregion - - //#region Check - - /** - * Checks if the given operation is possibly allowed by the policy, without querying the database. - * @param operation The CRUD operation. - * @param fieldValues Extra field value filters to be combined with the policy constraints. - */ - async check(args: PermissionCheckArgs): Promise { - return checkPermission(this.model, args, this.modelMeta, this.policyUtils, this.prisma, this.prismaModule); - } - - //#endregion - - //#region Utils - - private get shouldLogQuery() { - return !!this.options?.logPrismaQuery && this.logger.enabled('info'); - } - - private async runPostWriteChecks(postWriteChecks: PostWriteCheckRecord[], db: CrudContract) { - await Promise.all( - postWriteChecks.map(async ({ model, operation, uniqueFilter, preValue }) => - this.policyUtils.checkPolicyForUnique(model, uniqueFilter, operation, db, [], preValue) - ) - ); - } - - private requireBackLink(fieldInfo: FieldInfo) { - invariant(fieldInfo.backLink, `back link not found for field ${fieldInfo.name}`); - return requireField(this.modelMeta, fieldInfo.type, fieldInfo.backLink); - } - - private mergeToParent(parent: any, key: string, value: any) { - if (parent[key]) { - if (Array.isArray(parent[key])) { - parent[key].push(value); - } else { - parent[key] = [parent[key], value]; - } - } else { - parent[key] = value; - } - } - - private removeFromParent(parent: any, key: string, data: any) { - if (parent[key] === data) { - delete parent[key]; - } else if (Array.isArray(parent[key])) { - const idx = parent[key].indexOf(data); - if (idx >= 0) { - parent[key].splice(idx, 1); - if (parent[key].length === 0) { - delete parent[key]; - } - } - } - } - - private buildIdFilterWithEntityChecker(candidates: any[], entityChecker: EntityCheckerFunc) { - const filteredCandidates = candidates.filter((value) => entityChecker(value, { user: this.context?.user })); - const idFields = this.policyUtils.getIdFields(this.model); - let idFilter: any; - if (idFields.length === 1) { - idFilter = { [idFields[0].name]: { in: filteredCandidates.map((x) => x[idFields[0].name]) } }; - } else { - idFilter = { AND: filteredCandidates.map((x) => this.policyUtils.getIdFieldValues(this.model, x)) }; - } - return { filteredCandidates, idFilter }; - } - - //#endregion -} diff --git a/packages/runtime/src/enhancements/node/policy/index.ts b/packages/runtime/src/enhancements/node/policy/index.ts deleted file mode 100644 index d5523e31b..000000000 --- a/packages/runtime/src/enhancements/node/policy/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { getIdFields } from '../../../cross'; -import { DbClientContract, EnhancementContext } from '../../../types'; -import { hasAllFields } from '../../../validation'; -import type { InternalEnhancementOptions } from '../create-enhancement'; -import { Logger } from '../logger'; -import { makeProxy } from '../proxy'; -import { PolicyProxyHandler } from './handler'; -import { PolicyUtil } from './policy-utils'; - -/** - * Gets an enhanced Prisma client with access policy check. - * - * @param prisma The original Prisma client - * @param context The policy evaluation context - * @param policy The policy definition, will be loaded from default location if not provided - * @param modelMeta The model metadata, will be loaded from default location if not provided - * - * @private - */ -export function withPolicy( - prisma: DbClient, - options: InternalEnhancementOptions, - context?: EnhancementContext -): DbClient { - const { modelMeta, policy } = options; - - // validate user context - const userContext = context?.user; - if (userContext && modelMeta.authModel) { - const idFields = getIdFields(modelMeta, modelMeta.authModel); - if ( - !hasAllFields( - context.user, - idFields.map((f) => f.name) - ) - ) { - throw new Error( - `Invalid user context: must have valid ID field ${idFields.map((f) => `"${f.name}"`).join(', ')}` - ); - } - - // validate user context for fields used in policy expressions - const authSelector = policy.authSelector; - if (authSelector) { - Object.keys(authSelector).forEach((f) => { - if (!(f in userContext)) { - const logger = new Logger(prisma); - logger.warn(`User context does not have field "${f}" used in policy rules`); - } - }); - } - } - - return makeProxy( - prisma, - modelMeta, - (_prisma, model) => new PolicyProxyHandler(_prisma as DbClientContract, model, options, context), - 'policy', - options?.errorTransformer - ); -} - -/** - * Function for processing a payload for including a relation field in a query. - * @param model The relation's model name - * @param payload The payload to process - */ -export async function policyProcessIncludeRelationPayload( - prisma: DbClientContract, - model: string, - payload: unknown, - options: InternalEnhancementOptions, - context: EnhancementContext | undefined -) { - const utils = new PolicyUtil(prisma, options, context); - await utils.injectForRead(prisma, model, payload); - await utils.injectReadCheckSelect(model, payload); -} diff --git a/packages/runtime/src/enhancements/node/policy/logic-solver.d.ts b/packages/runtime/src/enhancements/node/policy/logic-solver.d.ts deleted file mode 100644 index d10e688f6..000000000 --- a/packages/runtime/src/enhancements/node/policy/logic-solver.d.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Type definitions for the `logic-solver` npm package. - */ -declare module 'logic-solver' { - /** - * A boolean formula. - */ - interface Formula {} - - /** - * The `TRUE` formula. - */ - const TRUE: Formula; - - /** - * The `FALSE` formula. - */ - const FALSE: Formula; - - /** - * Boolean equivalence. - */ - export function equiv(operand1: Formula, operand2: Formula): Formula; - - /** - * Bits equality. - */ - export function equalBits(bits1: Formula, bits2: Formula): Formula; - - /** - * Bits greater-than. - */ - export function greaterThan(bits1: Formula, bits2: Formula): Formula; - - /** - * Bits greater-than-or-equal. - */ - export function greaterThanOrEqual(bits1: Formula, bits2: Formula): Formula; - - /** - * Bits less-than. - */ - export function lessThan(bits1: Formula, bits2: Formula): Formula; - - /** - * Bits less-than-or-equal. - */ - export function lessThanOrEqual(bits1: Formula, bits2: Formula): Formula; - - /** - * Logical AND. - */ - export function and(...args: Formula[]): Formula; - - /** - * Logical OR. - */ - export function or(...args: Formula[]): Formula; - - /** - * Logical NOT. - */ - export function not(arg: Formula): Formula; - - /** - * Creates a bits variable with the given name and bit length. - */ - export function variableBits(baseName: string, N: number): Formula; - - /** - * Creates a constant bits formula from the given whole number. - */ - export function constantBits(wholeNumber: number): Formula; - - /** - * A solution to a constraint. - */ - interface Solution { - /** - * Returns a map of variable assignments. - */ - getMap(): object; - - /** - * Evaluates the given formula against the solution. - */ - evaluate(formula: Formula): unknown; - } - - /** - * A constraint solver. - */ - class Solver { - /** - * Adds constraints to the solver. - */ - require(...args: Formula[]): void; - - /** - * Adds negated constraints from the solver. - */ - forbid(...args: Formula[]): void; - - /** - * Solves the constraints. - */ - solve(): Solution; - } -} diff --git a/packages/runtime/src/enhancements/node/policy/policy-utils.ts b/packages/runtime/src/enhancements/node/policy/policy-utils.ts deleted file mode 100644 index ed5d95982..000000000 --- a/packages/runtime/src/enhancements/node/policy/policy-utils.ts +++ /dev/null @@ -1,1690 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import deepmerge from 'deepmerge'; -import type { z, ZodError, ZodObject, ZodSchema } from 'zod'; -import { z as zod3 } from 'zod/v3'; -import { z as zod4 } from 'zod/v4'; -import { CrudFailureReason, PrismaErrorCode } from '../../../constants'; -import { - clone, - enumerate, - getFields, - getModelFields, - resolveField, - zip, - type FieldInfo, - type ModelMeta, -} from '../../../cross'; -import { - getZodErrorMessage, - isPlainObject, - lowerCaseFirst, - simpleTraverse, - upperCaseFirst, -} from '../../../local-helpers'; -import { - AuthUser, - CrudContract, - DbClientContract, - EnhancementContext, - PolicyCrudKind, - PolicyOperationKind, - QueryContext, - ZodSchemas, -} from '../../../types'; -import { getVersion } from '../../../version'; -import type { InternalEnhancementOptions } from '../create-enhancement'; -import { QueryUtils } from '../query-utils'; -import type { - DelegateConstraint, - EntityChecker, - ModelPolicyDef, - PermissionCheckerFunc, - PolicyDef, - PolicyFunc, - VariableConstraint, -} from '../types'; -import { formatObject, prismaClientKnownRequestError } from '../utils'; - -/** - * Access policy enforcement utilities - */ -export class PolicyUtil extends QueryUtils { - private readonly modelMeta: ModelMeta; - private readonly policy: PolicyDef; - private readonly zodSchemas?: ZodSchemas; - private readonly prismaModule: any; - private readonly user?: AuthUser; - - constructor( - private readonly db: DbClientContract, - options: InternalEnhancementOptions, - context?: EnhancementContext, - private readonly shouldLogQuery = false - ) { - super(db, options); - - this.user = context?.user; - - ({ - modelMeta: this.modelMeta, - policy: this.policy, - zodSchemas: this.zodSchemas, - prismaModule: this.prismaModule, - } = options); - } - - //#region Logical operators - - /** - * Creates a conjunction of a list of query conditions. - */ - and(...conditions: (boolean | object | undefined)[]): object { - const filtered = conditions.filter((c) => c !== undefined); - if (filtered.length === 0) { - return this.makeTrue(); - } else if (filtered.length === 1) { - return this.reduce(filtered[0]); - } else { - return this.reduce({ AND: filtered }); - } - } - - /** - * Creates a disjunction of a list of query conditions. - */ - or(...conditions: (boolean | object | undefined)[]): object { - const filtered = conditions.filter((c) => c !== undefined); - if (filtered.length === 0) { - return this.makeFalse(); - } else if (filtered.length === 1) { - return this.reduce(filtered[0]); - } else { - return this.reduce({ OR: filtered }); - } - } - - /** - * Creates a negation of a query condition. - */ - not(condition: object | boolean | undefined): object { - if (condition === undefined) { - return this.makeTrue(); - } else if (typeof condition === 'boolean') { - return this.reduce(!condition); - } else { - return this.reduce({ NOT: condition }); - } - } - - // Static True/False conditions - // https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals - - private singleKey(obj: object | null | undefined, key: string): obj is { [key: string]: unknown } { - if (!obj) { - return false; - } else { - return Object.keys(obj).length === 1 && Object.keys(obj)[0] === key; - } - } - - public isTrue(condition: object | null | undefined) { - if (condition === null || condition === undefined || !isPlainObject(condition)) { - return false; - } - - // {} is true - if (Object.keys(condition).length === 0) { - return true; - } - - // { OR: TRUE } is true - if (this.singleKey(condition, 'OR') && typeof condition.OR === 'object' && this.isTrue(condition.OR)) { - return true; - } - - // { NOT: FALSE } is true - if (this.singleKey(condition, 'NOT') && typeof condition.NOT === 'object' && this.isFalse(condition.NOT)) { - return true; - } - - // { AND: [] } is true - if (this.singleKey(condition, 'AND') && Array.isArray(condition.AND) && condition.AND.length === 0) { - return true; - } - - return false; - } - - public isFalse(condition: object | null | undefined) { - if (condition === null || condition === undefined || !isPlainObject(condition)) { - return false; - } - - // { AND: FALSE } is false - if (this.singleKey(condition, 'AND') && typeof condition.AND === 'object' && this.isFalse(condition.AND)) { - return true; - } - - // { NOT: TRUE } is false - if (this.singleKey(condition, 'NOT') && typeof condition.NOT === 'object' && this.isTrue(condition.NOT)) { - return true; - } - - // { OR: [] } is false - if (this.singleKey(condition, 'OR') && Array.isArray(condition.OR) && condition.OR.length === 0) { - return true; - } - - return false; - } - - private makeTrue() { - return { AND: [] }; - } - - private makeFalse() { - return { OR: [] }; - } - - private reduce(condition: object | boolean | undefined): object { - if (condition === true || condition === undefined) { - return this.makeTrue(); - } - - if (condition === false) { - return this.makeFalse(); - } - - if (condition === null) { - return condition; - } - - const result: any = {}; - for (const [key, value] of Object.entries(condition)) { - if (value === null || value === undefined) { - result[key] = value; - continue; - } - - switch (key) { - case 'AND': { - const children = enumerate(value) - .map((c: any) => this.reduce(c)) - .filter((c) => c !== undefined && !this.isTrue(c)); - if (children.length === 0) { - // { ..., AND: [] } - result[key] = []; - } else if (children.some((c) => this.isFalse(c))) { - // { ..., AND: { OR: [] } } - result[key] = this.makeFalse(); - } else { - result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children; - } - break; - } - - case 'OR': { - const children = enumerate(value) - .map((c: any) => this.reduce(c)) - .filter((c) => c !== undefined && !this.isFalse(c)); - if (children.length === 0) { - // { ..., OR: [] } - result[key] = []; - } else if (children.some((c) => this.isTrue(c))) { - // { ..., OR: { AND: [] } } - result[key] = this.makeTrue(); - } else { - result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children; - } - break; - } - - case 'NOT': { - const children = enumerate(value).map((c: any) => this.reduce(c)); - result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children; - break; - } - - default: { - if (!isPlainObject(value)) { - // don't visit into non-plain object values - could be Date, array, etc. - result[key] = value; - } else { - result[key] = this.reduce(value); - } - break; - } - } - } - - // finally normalize constant true/false conditions - if (this.isTrue(result)) { - return this.makeTrue(); - } else if (this.isFalse(result)) { - return this.makeFalse(); - } else { - return result; - } - } - - //#endregion - - //#region Auth guard - - private readonly FULL_OPEN_MODEL_POLICY: ModelPolicyDef = { - modelLevel: { - read: { guard: true }, - create: { guard: true, inputChecker: true }, - update: { guard: true }, - delete: { guard: true }, - postUpdate: { guard: true }, - }, - }; - - private getModelPolicyDef(model: string): ModelPolicyDef { - if (this.options.kinds && !this.options.kinds.includes('policy')) { - // policy enhancement not enabled, return an fully open guard - return this.FULL_OPEN_MODEL_POLICY; - } - - const def = this.policy.policy[lowerCaseFirst(model)]; - if (!def) { - throw this.unknownError(`unable to load policy guard for ${model}`); - } - return def; - } - - private getModelGuardForOperation(model: string, operation: PolicyOperationKind): PolicyFunc | boolean { - const def = this.getModelPolicyDef(model); - return def.modelLevel[operation].guard ?? true; - } - - /** - * Gets pregenerated authorization guard object for a given model and operation. - * - * @returns true if operation is unconditionally allowed, false if unconditionally denied, - * otherwise returns a guard object - */ - getAuthGuard(db: CrudContract, model: string, operation: PolicyOperationKind, preValue?: any) { - const guard = this.getModelGuardForOperation(model, operation); - - // constant guard - if (typeof guard === 'boolean') { - return this.reduce(guard); - } - - // invoke guard function - const r = guard({ user: this.user, preValue }, db); - return this.reduce(r); - } - - /** - * Get field-level read auth guard - */ - getFieldReadAuthGuard(db: CrudContract, model: string, field: string) { - const def = this.getModelPolicyDef(model); - const guard = def.fieldLevel?.read?.[field]?.guard; - - if (guard === undefined) { - // field access is allowed by default - return this.makeTrue(); - } - - if (typeof guard === 'boolean') { - return this.reduce(guard); - } - - const r = guard({ user: this.user }, db); - return this.reduce(r); - } - - /** - * Get field-level read auth guard that overrides the model-level - */ - getFieldOverrideReadAuthGuard(db: CrudContract, model: string, field: string) { - const def = this.getModelPolicyDef(model); - const guard = def.fieldLevel?.read?.[field]?.overrideGuard; - - if (guard === undefined) { - // field access is denied by default in override mode - return this.makeFalse(); - } - - if (typeof guard === 'boolean') { - return this.reduce(guard); - } - - const r = guard({ user: this.user }, db); - return this.reduce(r); - } - - /** - * Get field-level update auth guard - */ - getFieldUpdateAuthGuard(db: CrudContract, model: string, field: string) { - const def = this.getModelPolicyDef(model); - const guard = def.fieldLevel?.update?.[field]?.guard; - - if (guard === undefined) { - // field access is allowed by default - return this.makeTrue(); - } - - if (typeof guard === 'boolean') { - return this.reduce(guard); - } - - const r = guard({ user: this.user }, db); - return this.reduce(r); - } - - /** - * Get field-level update auth guard that overrides the model-level - */ - getFieldOverrideUpdateAuthGuard(db: CrudContract, model: string, field: string) { - const def = this.getModelPolicyDef(model); - const guard = def.fieldLevel?.update?.[field]?.overrideGuard; - - if (guard === undefined) { - // field access is denied by default in override mode - return this.makeFalse(); - } - - if (typeof guard === 'boolean') { - return this.reduce(guard); - } - - const r = guard({ user: this.user }, db); - return this.reduce(r); - } - - /** - * Checks if the given model has a policy guard for the given operation. - */ - hasAuthGuard(model: string, operation: PolicyOperationKind) { - const guard = this.getModelGuardForOperation(model, operation); - return typeof guard !== 'boolean' || guard !== true; - } - - /** - * Checks if the given model has any field-level override policy guard for the given operation. - */ - hasOverrideAuthGuard(model: string, operation: PolicyOperationKind) { - if (operation !== 'read' && operation !== 'update') { - return false; - } - const def = this.getModelPolicyDef(model); - if (def.fieldLevel?.[operation]) { - return Object.values(def.fieldLevel[operation]).some( - (f) => f.overrideGuard !== undefined || f.overrideEntityChecker !== undefined - ); - } else { - return false; - } - } - - /** - * Checks model creation policy based on static analysis to the input args. - * - * @returns boolean if static analysis is enough to determine the result, undefined if not - */ - checkInputGuard(model: string, args: any, operation: 'create'): boolean | undefined { - const def = this.getModelPolicyDef(model); - - const guard = def.modelLevel[operation].inputChecker; - if (guard === undefined) { - return undefined; - } - - if (typeof guard === 'boolean') { - return guard; - } - - return guard(args, { user: this.user }); - } - - /** - * Injects model auth guard as where clause. - */ - injectAuthGuardAsWhere(db: CrudContract, args: any, model: string, operation: PolicyOperationKind) { - let guard = this.getAuthGuard(db, model, operation); - - if (operation === 'update' && args) { - // merge field-level policy guards - const fieldUpdateGuard = this.getFieldUpdateGuards( - db, - model, - this.getFieldsWithDefinedValues(args.data ?? args) - ); - if (fieldUpdateGuard.rejectedByField) { - // rejected - args.where = this.makeFalse(); - return false; - } else { - if (fieldUpdateGuard.guard) { - // merge field-level guard - guard = this.and(guard, fieldUpdateGuard.guard); - } - - if (fieldUpdateGuard.overrideGuard) { - // merge field-level override guard on the top level - guard = this.or(guard, fieldUpdateGuard.overrideGuard); - } - } - } - - if (operation === 'read') { - // merge field-level read override guards - const fieldReadOverrideGuard = this.getCombinedFieldOverrideReadGuards(db, model, args); - if (fieldReadOverrideGuard) { - guard = this.or(guard, fieldReadOverrideGuard); - } - } - - if (this.isFalse(guard)) { - args.where = this.makeFalse(); - return false; - } - - let mergedGuard = guard; - if (args.where) { - // inject into fields: - // to-many: some/none/every - // to-one: direct-conditions/is/isNot - // regular fields - mergedGuard = this.buildReadGuardForFields(db, model, args.where, guard); - } - - args.where = this.and(args.where, mergedGuard); - return true; - } - - // Injects guard for relation fields nested in `payload`. The `modelGuard` parameter represents the model-level guard for `model`. - // The function returns a modified copy of `modelGuard` with field-level policies combined. - private buildReadGuardForFields(db: CrudContract, model: string, payload: any, modelGuard: any) { - if (!payload || typeof payload !== 'object' || Object.keys(payload).length === 0) { - return modelGuard; - } - - const allFieldGuards: object[] = []; - const allFieldOverrideGuards: object[] = []; - - for (const [field, subPayload] of Object.entries(payload)) { - if (!subPayload) { - continue; - } - - allFieldGuards.push(this.getFieldReadAuthGuard(db, model, field)); - allFieldOverrideGuards.push(this.getFieldOverrideReadAuthGuard(db, model, field)); - - const fieldInfo = resolveField(this.modelMeta, model, field); - if (fieldInfo?.isDataModel) { - if (fieldInfo.isArray) { - this.injectReadGuardForToManyField(db, fieldInfo, subPayload); - } else { - this.injectReadGuardForToOneField(db, fieldInfo, subPayload); - } - } - } - - // all existing field-level guards must be true - const mergedGuard: object = this.and(...allFieldGuards); - - // all existing field-level override guards must be true for override to take effect; override is disabled by default - const mergedOverrideGuard: object = - allFieldOverrideGuards.length === 0 ? this.makeFalse() : this.and(...allFieldOverrideGuards); - - // (original-guard && field-level-guard) || field-level-override-guard - const updatedGuard = this.or(this.and(modelGuard, mergedGuard), mergedOverrideGuard); - return updatedGuard; - } - - private injectReadGuardForToManyField( - db: CrudContract, - fieldInfo: FieldInfo, - payload: { some?: any; every?: any; none?: any } - ) { - const guard = this.getAuthGuard(db, fieldInfo.type, 'read'); - if (payload.some) { - const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.some, guard); - // turn "some" into: { some: { AND: [guard, payload.some] } } - payload.some = this.and(payload.some, mergedGuard); - } - if (payload.none) { - const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.none, guard); - // turn none into: { none: { AND: [guard, payload.none] } } - payload.none = this.and(payload.none, mergedGuard); - } - if ( - payload.every && - typeof payload.every === 'object' && - // ignore empty every clause - Object.keys(payload.every).length > 0 - ) { - const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.every, guard); - - // turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } } - if (!payload.none) { - payload.none = {}; - } - payload.none = this.and(payload.none, mergedGuard, this.not(payload.every)); - delete payload.every; - } - } - - private injectReadGuardForToOneField( - db: CrudContract, - fieldInfo: FieldInfo, - payload: { is?: any; isNot?: any } & Record - ) { - const guard = this.getAuthGuard(db, fieldInfo.type, 'read'); - - // is|isNot and flat fields conditions are mutually exclusive - - // is and isNot can be null value - - if (payload.is !== undefined || payload.isNot !== undefined) { - if (payload.is) { - const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.is, guard); - // merge guard with existing "is": { is: { AND: [originalIs, guard] } } - payload.is = this.and(payload.is, mergedGuard); - } - - if (payload.isNot) { - const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.isNot, guard); - // merge guard with existing "isNot": { isNot: { AND: [originalIsNot, guard] } } - payload.isNot = this.and(payload.isNot, mergedGuard); - } - } else { - const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload, guard); - // turn direct conditions into: { is: { AND: [ originalConditions, guard ] } } - const combined = this.and(clone(payload), mergedGuard); - Object.keys(payload).forEach((key) => delete payload[key]); - payload.is = combined; - } - } - - /** - * Injects auth guard for read operations. - */ - injectForRead(db: CrudContract, model: string, args: any) { - // make select and include visible to the injection - const injected: any = { select: args.select, include: args.include }; - if (!this.injectAuthGuardAsWhere(db, injected, model, 'read')) { - args.where = this.makeFalse(); - return false; - } - - if (args.where) { - // visit fields accessed in where clause and merge field-level policies, - // fields are only allowed in where if they satisfy field-level read policies. - const mergedGuard = this.buildReadGuardForFields(db, model, args.where, {}); - args.where = this.mergeWhereClause(args.where, mergedGuard); - } - - if (args.where) { - if (injected.where && Object.keys(injected.where).length > 0) { - // merge injected guard with the user-provided where clause - args.where = this.mergeWhereClause(args.where, injected.where); - } - } else if (injected.where) { - // no user-provided where clause, use the injected one - args.where = injected.where; - } - - // recursively inject read guard conditions into nested select, include, and _count - const hoistedConditions = this.injectNestedReadConditions(db, model, args); - - // the injection process may generate conditions that need to be hoisted to the toplevel, - // if so, merge it with the existing where - if (hoistedConditions.length > 0) { - if (!args.where) { - args.where = this.and(...hoistedConditions); - } else { - args.where = this.mergeWhereClause(args.where, this.and(...hoistedConditions)); - } - } - - return true; - } - - //#endregion - - //#region Checker - - /** - * Gets checker constraints for the given model and operation. - */ - getCheckerConstraint(model: string, operation: PolicyCrudKind): ReturnType | boolean { - if (this.options.kinds && !this.options.kinds.includes('policy')) { - // policy enhancement not enabled, return a constant true checker result - return true; - } - - const def = this.getModelPolicyDef(model); - const checker = def.modelLevel[operation].permissionChecker; - if (checker === undefined) { - throw new Error( - `Generated permission checkers not found. Please make sure the "generatePermissionChecker" option is set to true in the "@core/enhancer" plugin.` - ); - } - - if (typeof checker === 'boolean') { - return checker; - } - - if (typeof checker !== 'function') { - throw this.unknownError(`invalid ${operation} checker function for ${model}`); - } - - // call checker function - let result = checker({ user: this.user }); - - // the constraint may contain "delegate" ones that should be resolved - // by evaluating the corresponding checker of the delegated models - - const isVariableConstraint = (value: any): value is VariableConstraint => { - return value && typeof value === 'object' && value.kind === 'variable'; - }; - - const isDelegateConstraint = (value: any): value is DelegateConstraint => { - return value && typeof value === 'object' && value.kind === 'delegate'; - }; - - // here we prefix the constraint variables coming from delegated checkers - // with the relation field name to avoid conflicts - const prefixConstraintVariables = (constraint: unknown, prefix: string) => { - return simpleTraverse(constraint, ({ value, update }) => { - if (isVariableConstraint(value)) { - update({ - ...value, - name: `${prefix}${value.name}`, - }); - } - }); - }; - - result = simpleTraverse(result, ({ value, update }) => { - if (isDelegateConstraint(value)) { - const { model: delegateModel, relation, operation: delegateOp } = value; - let newValue = this.getCheckerConstraint(delegateModel, delegateOp ?? operation); - newValue = prefixConstraintVariables(newValue, `${relation}.`); - update(newValue); - } - }); - - return result; - } - - //#endregion - - /** - * Gets unique constraints for the given model. - */ - getUniqueConstraints(model: string) { - return this.modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints ?? {}; - } - - private injectNestedReadConditions(db: CrudContract, model: string, args: any): any[] { - const injectTarget = args.select ?? args.include; - if (!injectTarget) { - return []; - } - - if (injectTarget._count !== undefined) { - // _count needs to respect read policies of related models - if (injectTarget._count === true) { - // include count for all relations, expand to all fields - // so that we can inject guard conditions for each of them - injectTarget._count = { select: {} }; - const modelFields = getFields(this.modelMeta, model); - if (modelFields) { - for (const [k, v] of Object.entries(modelFields)) { - if (v.isDataModel && v.isArray) { - // create an entry for to-many relation - injectTarget._count.select[k] = {}; - } - } - } - } - - // inject conditions for each relation - for (const field of Object.keys(injectTarget._count.select)) { - if (typeof injectTarget._count.select[field] !== 'object') { - injectTarget._count.select[field] = {}; - } - const fieldInfo = resolveField(this.modelMeta, model, field); - if (!fieldInfo) { - continue; - } - // inject into the "where" clause inside select - this.injectAuthGuardAsWhere(db, injectTarget._count.select[field], fieldInfo.type, 'read'); - } - } - - // collect filter conditions that should be hoisted to the toplevel - const hoistedConditions: any[] = []; - - for (const field of getModelFields(injectTarget)) { - if (injectTarget[field] === false) { - continue; - } - - const fieldInfo = resolveField(this.modelMeta, model, field); - if (!fieldInfo || !fieldInfo.isDataModel) { - // only care about relation fields - continue; - } - - let hoisted: any; - - if ( - fieldInfo.isArray || - // Injecting where at include/select level for nullable to-one relation is supported since Prisma 4.8.0 - // https://github.com/prisma/prisma/discussions/20350 - fieldInfo.isOptional - ) { - if (typeof injectTarget[field] !== 'object') { - injectTarget[field] = {}; - } - // inject extra condition for to-many or nullable to-one relation - this.injectAuthGuardAsWhere(db, injectTarget[field], fieldInfo.type, 'read'); - - // recurse - const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]); - if (subHoisted.length > 0) { - // we can convert it to a where at this level - injectTarget[field].where = this.and(injectTarget[field].where, ...subHoisted); - } - } else { - // hoist non-nullable to-one filter to the parent level - let injected = this.safeClone(injectTarget[field]); - if (typeof injected !== 'object') { - injected = {}; - } - this.injectAuthGuardAsWhere(db, injected, fieldInfo.type, 'read'); - hoisted = injected.where; - - // recurse - const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]); - if (subHoisted.length > 0) { - hoisted = this.and(hoisted, ...subHoisted); - } - } - - if (hoisted && !this.isTrue(hoisted)) { - hoistedConditions.push({ [field]: hoisted }); - } - } - - return hoistedConditions; - } - - /** - * Given a model and a unique filter, checks the operation is allowed by policies and field validations. - * Rejects with an error if not allowed. - * - * This method is only called by mutation operations. - */ - async checkPolicyForUnique( - model: string, - uniqueFilter: any, - operation: PolicyOperationKind, - db: CrudContract, - fieldsToUpdate: string[], - preValue?: any - ) { - let guard = this.getAuthGuard(db, model, operation, preValue); - if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { - throw this.deniedByPolicy( - model, - operation, - `entity ${formatObject(uniqueFilter, false)} failed policy check`, - CrudFailureReason.ACCESS_POLICY_VIOLATION - ); - } - - let entityChecker: EntityChecker | undefined; - - if (operation === 'update' && fieldsToUpdate.length > 0) { - // merge field-level policy guards - const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, fieldsToUpdate); - if (fieldUpdateGuard.rejectedByField) { - // rejected - throw this.deniedByPolicy( - model, - 'update', - `entity ${formatObject(uniqueFilter, false)} failed update policy check for field "${ - fieldUpdateGuard.rejectedByField - }"`, - CrudFailureReason.ACCESS_POLICY_VIOLATION - ); - } - - if (fieldUpdateGuard.guard) { - // merge field-level guard with AND - guard = this.and(guard, fieldUpdateGuard.guard); - } - - if (fieldUpdateGuard.overrideGuard) { - // merge field-level override guard with OR - guard = this.or(guard, fieldUpdateGuard.overrideGuard); - } - - // field-level entity checker - entityChecker = fieldUpdateGuard.entityChecker; - } - - // Zod schema is to be checked for "create" and "postUpdate" - const schema = ['create', 'postUpdate'].includes(operation) ? this.getZodSchema(model) : undefined; - - // combine field-level entity checker with model-level - const modelEntityChecker = this.getEntityChecker(model, operation); - entityChecker = this.combineEntityChecker(entityChecker, modelEntityChecker, 'and'); - - if (this.isTrue(guard) && !schema && !entityChecker) { - // unconditionally allowed - return; - } - - let select = schema - ? // need to validate against schema, need to fetch all fields - undefined - : // only fetch id fields - this.makeIdSelection(model); - - if (entityChecker?.selector) { - if (!select) { - select = this.makeAllScalarFieldSelect(model); - } - select = { ...select, ...entityChecker.selector }; - } - - let where = this.safeClone(uniqueFilter); - // query args may have be of combined-id form, need to flatten it to call findFirst - this.flattenGeneratedUniqueField(model, where); - - // query with policy guard - where = this.and(where, guard); - const query = { select, where }; - - if (this.shouldLogQuery) { - this.logger.info(`[policy] checking ${model} for ${operation}, \`findFirst\`:\n${formatObject(query)}`); - } - const result = await db[model].findFirst(query); - if (!result) { - throw this.deniedByPolicy( - model, - operation, - `entity ${formatObject(uniqueFilter, false)} failed policy check`, - CrudFailureReason.ACCESS_POLICY_VIOLATION - ); - } - - if (entityChecker) { - if (this.logger.enabled('info')) { - this.logger.info(`[policy] running entity checker on ${model} for ${operation}`); - } - if (!entityChecker.func(result, { user: this.user, preValue })) { - throw this.deniedByPolicy( - model, - operation, - `entity ${formatObject(uniqueFilter, false)} failed policy check`, - CrudFailureReason.ACCESS_POLICY_VIOLATION - ); - } - } - - if (schema && !this.options.validation?.inputOnlyValidationForUpdate) { - // TODO: push down schema check to the database - this.validateZodSchema(model, undefined, result, true, (err) => { - throw this.deniedByPolicy( - model, - operation, - `entity ${formatObject(uniqueFilter, false)} failed validation: [${getZodErrorMessage(err)}]`, - CrudFailureReason.DATA_VALIDATION_VIOLATION, - err - ); - }); - } - } - - getEntityChecker(model: string, operation: PolicyOperationKind, field?: string) { - const def = this.getModelPolicyDef(model); - if (field) { - return def.fieldLevel?.[operation as 'read' | 'update']?.[field]?.entityChecker; - } else { - return def.modelLevel[operation].entityChecker; - } - } - - getUpdateOverrideEntityCheckerForField(model: string, field: string) { - const def = this.getModelPolicyDef(model); - return def.fieldLevel?.update?.[field]?.overrideEntityChecker; - } - - // visit fields referenced in select/include and return a combined field-level override read guard - private getCombinedFieldOverrideReadGuards(db: CrudContract, model: string, args: { select?: any; include?: any }) { - const allFields = Object.values(getFields(this.modelMeta, model)); - - // all scalar fields by default - let fields = allFields.filter((f) => !f.isDataModel); - - if (args.select) { - // explicitly selected fields - fields = allFields.filter((f) => args.select?.[f.name] === true); - } else if (args.include) { - // included relations - fields.push(...allFields.filter((f) => !fields.includes(f) && args.include[f.name])); - } - - if (fields.length === 0) { - // this can happen if only selecting pseudo fields like "_count" - return undefined; - } - - const allFieldGuards = fields.map((field) => this.getFieldOverrideReadAuthGuard(db, model, field.name)); - return this.and(...allFieldGuards); - } - - private getFieldUpdateGuards(db: CrudContract, model: string, fieldsToUpdate: string[]) { - const allFieldGuards = []; - const allOverrideFieldGuards = []; - let entityChecker: EntityChecker | undefined; - - for (const field of fieldsToUpdate) { - const fieldInfo = resolveField(this.modelMeta, model, field); - - if (fieldInfo?.isDataModel) { - // relation field update should be treated as foreign key update, - // fetch and merge all foreign key guards - if (fieldInfo.isRelationOwner && fieldInfo.foreignKeyMapping) { - const foreignKeys = Object.values(fieldInfo.foreignKeyMapping); - for (const fk of foreignKeys) { - const fieldGuard = this.getFieldUpdateAuthGuard(db, model, fk); - if (this.isFalse(fieldGuard)) { - return { guard: fieldGuard, rejectedByField: fk }; - } - - // add field guard - allFieldGuards.push(fieldGuard); - - // add field override guard - const overrideFieldGuard = this.getFieldOverrideUpdateAuthGuard(db, model, fk); - allOverrideFieldGuards.push(overrideFieldGuard); - } - } - } else { - const fieldGuard = this.getFieldUpdateAuthGuard(db, model, field); - if (this.isFalse(fieldGuard)) { - return { guard: fieldGuard, rejectedByField: field }; - } - - // add field guard - allFieldGuards.push(fieldGuard); - - // add field override guard - const overrideFieldGuard = this.getFieldOverrideUpdateAuthGuard(db, model, field); - allOverrideFieldGuards.push(overrideFieldGuard); - } - - // merge regular and override entity checkers with OR - let checker = this.getEntityChecker(model, 'update', field); - const overrideChecker = this.getUpdateOverrideEntityCheckerForField(model, field); - checker = this.combineEntityChecker(checker, overrideChecker, 'or'); - - // accumulate entity checker across fields - entityChecker = this.combineEntityChecker(entityChecker, checker, 'and'); - } - - const allFieldsCombined = this.and(...allFieldGuards); - const allOverrideFieldsCombined = - allOverrideFieldGuards.length !== 0 ? this.and(...allOverrideFieldGuards) : undefined; - - return { - guard: allFieldsCombined, - overrideGuard: allOverrideFieldsCombined, - rejectedByField: undefined, - entityChecker, - }; - } - - private combineEntityChecker( - left: EntityChecker | undefined, - right: EntityChecker | undefined, - combiner: 'and' | 'or' - ): EntityChecker | undefined { - if (!left) { - return right; - } - - if (!right) { - return left; - } - - const func = - combiner === 'and' - ? (entity: any, context: QueryContext) => left.func(entity, context) && right.func(entity, context) - : (entity: any, context: QueryContext) => left.func(entity, context) || right.func(entity, context); - - return { - func, - selector: deepmerge(left.selector ?? {}, right.selector ?? {}), - }; - } - - /** - * Tries rejecting a request based on static "false" policy. - */ - tryReject(db: CrudContract, model: string, operation: PolicyOperationKind) { - const guard = this.getAuthGuard(db, model, operation); - if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { - throw this.deniedByPolicy(model, operation, undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); - } - } - - /** - * Checks if a model exists given a unique filter. - */ - async checkExistence(db: CrudContract, model: string, uniqueFilter: any, throwIfNotFound = false): Promise { - uniqueFilter = this.safeClone(uniqueFilter); - this.flattenGeneratedUniqueField(model, uniqueFilter); - - if (this.shouldLogQuery) { - this.logger.info(`[policy] checking ${model} existence, \`findFirst\`:\n${formatObject(uniqueFilter)}`); - } - const existing = await db[model].findFirst({ - where: uniqueFilter, - select: this.makeIdSelection(model), - }); - if (!existing && throwIfNotFound) { - throw this.notFound(model); - } - return existing; - } - - /** - * Returns an entity given a unique filter with read policy checked. Reject if not readable. - */ - async readBack( - db: CrudContract, - model: string, - operation: PolicyOperationKind, - selectInclude: { select?: any; include?: any }, - uniqueFilter: any - ): Promise<{ result: unknown; error?: Error }> { - uniqueFilter = this.safeClone(uniqueFilter); - this.flattenGeneratedUniqueField(model, uniqueFilter); - - // make sure only select and include are picked - const selectIncludeClean = this.pick(selectInclude, 'select', 'include'); - const readArgs = { - ...this.safeClone(selectIncludeClean), - where: uniqueFilter, - }; - - const error = this.deniedByPolicy( - model, - operation, - 'result is not allowed to be read back', - CrudFailureReason.RESULT_NOT_READABLE - ); - - const injectResult = this.injectForRead(db, model, readArgs); - if (!injectResult) { - return { error, result: undefined }; - } - - // inject select needed for field-level read checks - this.injectReadCheckSelect(model, readArgs); - - if (this.shouldLogQuery) { - this.logger.info(`[policy] checking read-back, \`findFirst\` ${model}:\n${formatObject(readArgs)}`); - } - const result = await db[model].findFirst(readArgs); - if (!result) { - if (this.shouldLogQuery) { - this.logger.info(`[policy] cannot read back ${model}`); - } - return { error, result: undefined }; - } - - this.postProcessForRead(result, model, selectIncludeClean); - return { result, error: undefined }; - } - - /** - * Injects field selection needed for checking field-level read policy check and evaluating - * entity checker into query args. - */ - injectReadCheckSelect(model: string, args: any) { - // we need to recurse into relation fields before injecting the current level, because - // injection into current level can result in relation being selected/included, which - // can then cause infinite recursion when we visit relation later - - // recurse into relation fields - const visitTarget = args.select ?? args.include; - if (visitTarget) { - for (const key of Object.keys(visitTarget)) { - const field = resolveField(this.modelMeta, model, key); - if (field?.isDataModel && visitTarget[key]) { - if (typeof visitTarget[key] !== 'object') { - // v is "true", ensure it's an object - visitTarget[key] = {}; - } - this.injectReadCheckSelect(field.type, visitTarget[key]); - } - } - } - - if (this.hasFieldLevelPolicy(model)) { - // recursively inject selection for fields needed for field-level read checks - const readFieldSelect = this.getFieldReadCheckSelector(model, args.select); - if (readFieldSelect) { - this.doInjectReadCheckSelect(model, args, { select: readFieldSelect }); - } - } - - const entityChecker = this.getEntityChecker(model, 'read'); - if (entityChecker?.selector) { - this.doInjectReadCheckSelect(model, args, { select: entityChecker.selector }); - } - } - - private doInjectReadCheckSelect(model: string, args: any, input: any) { - // omit should be ignored to avoid interfering with field selection - if (args.omit) { - delete args.omit; - } - - if (!input?.select) { - return; - } - - // process scalar field selections first - for (const [k, v] of Object.entries(input.select)) { - const field = resolveField(this.modelMeta, model, k); - if (!field || field.isDataModel) { - continue; - } - if (v === true) { - if (!args.select) { - // do nothing since all scalar fields are selected by default - } else if (args.include) { - // do nothing since include implies selecting all scalar fields - } else { - args.select[k] = true; - } - } - } - - // process relation selections - for (const [k, v] of Object.entries(input.select)) { - const field = resolveField(this.modelMeta, model, k); - if (!field || !field.isDataModel) { - continue; - } - - // prepare the next level of args - let nextArgs = args.select ?? args.include; - if (!nextArgs) { - nextArgs = args.include = {}; - } - if (!nextArgs[k] || typeof nextArgs[k] !== 'object') { - nextArgs[k] = {}; - } - - if (v && typeof v === 'object') { - // recurse into relation - this.doInjectReadCheckSelect(field.type, nextArgs[k], v); - } - } - } - - private makeAllScalarFieldSelect(model: string): any { - const fields = this.getModelFields(model); - const result: any = {}; - if (fields) { - Object.entries(fields).forEach(([k, v]) => { - if (!v.isDataModel) { - result[k] = true; - } - }); - } - return result; - } - - //#endregion - - //#region Errors - - deniedByPolicy( - model: string, - operation: PolicyOperationKind, - extra?: string, - reason?: CrudFailureReason, - zodErrors?: ZodError - ) { - const args: any = { clientVersion: getVersion(), code: PrismaErrorCode.CONSTRAINT_FAILED, meta: {} }; - if (reason) { - args.meta.reason = reason; - } - - if (zodErrors) { - args.meta.zodErrors = zodErrors; - } - - return prismaClientKnownRequestError( - this.db, - this.prismaModule, - `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, - args - ); - } - - notFound(model: string) { - return prismaClientKnownRequestError(this.db, this.prismaModule, `entity not found for model ${model}`, { - clientVersion: getVersion(), - code: 'P2025', - }); - } - - //#endregion - - //#region Misc - - /** - * Gets field selection for fetching pre-update entity values for the given model. - */ - getPreValueSelect(model: string) { - const def = this.getModelPolicyDef(model); - return def.modelLevel.postUpdate.preUpdateSelector; - } - - // get a merged selector object for all field-level read policies - private getFieldReadCheckSelector(model: string, fieldSelection: Record | undefined) { - const def = this.getModelPolicyDef(model); - let result: any = {}; - const fieldLevel = def.fieldLevel?.read; - if (fieldLevel) { - for (const [field, def] of Object.entries(fieldLevel)) { - if (!fieldSelection || fieldSelection[field]) { - // field is selected, merge the field-level selector - if (def.entityChecker?.selector) { - result = deepmerge(result, def.entityChecker.selector); - } - if (def.overrideEntityChecker?.selector) { - result = deepmerge(result, def.overrideEntityChecker.selector); - } - } - } - } - return Object.keys(result).length > 0 ? result : undefined; - } - - private checkReadField(model: string, field: string, entity: any) { - const def = this.getModelPolicyDef(model); - - // combine regular and override field-level entity checkers with OR - const checker = def.fieldLevel?.read?.[field]?.entityChecker; - const overrideChecker = def.fieldLevel?.read?.[field]?.overrideEntityChecker; - const combinedChecker = this.combineEntityChecker(checker, overrideChecker, 'or'); - - if (combinedChecker === undefined) { - return true; - } else { - return combinedChecker.func(entity, { user: this.user }); - } - } - - private hasFieldValidation(model: string): boolean { - return this.policy.validation?.[lowerCaseFirst(model)]?.hasValidation === true; - } - - private hasFieldLevelPolicy(model: string) { - const def = this.getModelPolicyDef(model); - return Object.keys(def.fieldLevel?.read ?? {}).length > 0; - } - - /** - * Gets Zod schema for the given model and access kind. - * - * @param kind kind of Zod schema to get for. If undefined, returns the full schema. - */ - getZodSchema( - model: string, - excludePasswordFields: boolean = true, - kind: 'create' | 'update' | undefined = undefined - ) { - if (!this.zodSchemas) { - return undefined; - } - - if (!this.hasFieldValidation(model)) { - return undefined; - } - - const schemaKey = `${upperCaseFirst(model)}${kind ? 'Prisma' + upperCaseFirst(kind) : ''}Schema`; - - if (excludePasswordFields) { - // The `excludePasswordFields` mode is to handle the issue the fields marked with `@password` change at runtime, - // so they can only be fully validated when processing the input of "create" and "update" operations. - // - // When excluding them, we need to override them with plain string schemas. However, since the scheme is not always - // an `ZodObject` (this happens when there's `@@validate` refinement), we need to fetch the `ZodObject` schema before - // the refinement is applied, override the `@password` fields and then re-apply the refinement. - - let schema: ZodObject | undefined; - - const overridePasswordFields = (schema: z.ZodObject) => { - const useZod: any = schema._def ? zod3 : zod4; - - let result = schema; - const modelFields = this.modelMeta.models[lowerCaseFirst(model)]?.fields; - if (modelFields) { - for (const [key, field] of Object.entries(modelFields)) { - if (field.attributes?.some((attr) => attr.name === '@password')) { - // override `@password` field schema with a string schema - let pwFieldSchema: ZodSchema = useZod.string(); - if (field.isOptional) { - pwFieldSchema = pwFieldSchema.nullish(); - } - result = result.merge(useZod.object({ [key]: pwFieldSchema })); - } - } - } - return result; - }; - - // get the schema without refinement: `[Model]WithoutRefineSchema` - const withoutRefineSchemaKey = `${upperCaseFirst(model)}${ - kind ? 'Prisma' + upperCaseFirst(kind) : '' - }WithoutRefineSchema`; - schema = this.zodSchemas.models[withoutRefineSchemaKey] as ZodObject | undefined; - - if (schema) { - // the schema has refinement, need to call refine function after schema merge - schema = overridePasswordFields(schema); - // refine function: `refine[Model]` - const refineFuncKey = `refine${upperCaseFirst(model)}`; - const refineFunc = this.zodSchemas.models[refineFuncKey] as unknown as ( - schema: ZodObject - ) => ZodSchema; - return typeof refineFunc === 'function' ? refineFunc(schema) : schema; - } else { - // otherwise, directly override the `@password` fields - schema = this.zodSchemas.models[schemaKey] as ZodObject | undefined; - return schema ? overridePasswordFields(schema) : undefined; - } - } else { - // simply return the schema - return this.zodSchemas.models[schemaKey]; - } - } - - /** - * Validates the given data against the Zod schema for the given model and kind. - * - * @param model model - * @param kind validation kind. Pass undefined to validate against the full schema. - * @param data input data - * @param excludePasswordFields whether exclude schema validation for `@password` fields - * @param onError error callback - * @returns Zod-validated data - */ - validateZodSchema( - model: string, - kind: 'create' | 'update' | undefined, - data: object, - excludePasswordFields: boolean, - onError: (error: ZodError) => void - ) { - const schema = this.getZodSchema(model, excludePasswordFields, kind); - if (!schema) { - return data; - } - - const parseResult = schema.safeParse(data); - if (!parseResult.success) { - if (this.logger.enabled('info')) { - this.logger.info( - `entity ${model} failed validation for operation ${kind}: ${getZodErrorMessage(parseResult.error)}` - ); - } - onError(parseResult.error); - return undefined; - } - - return parseResult.data; - } - - /** - * Post processing checks and clean-up for read model entities. - */ - postProcessForRead(data: any, model: string, queryArgs: any) { - // preserve the original data as it may be needed for checking field-level readability, - // while the "data" will be manipulated during traversal (deleting unreadable fields) - const origData = this.safeClone(data); - - // use the concrete model if the data is a polymorphic entity - const realModel = this.getDelegateConcreteModel(model, data); - return this.doPostProcessForRead(data, realModel, origData, queryArgs, this.hasFieldLevelPolicy(realModel)); - } - - private doPostProcessForRead( - data: any, - model: string, - fullData: any, - queryArgs: any, - hasFieldLevelPolicy: boolean, - path = '' - ) { - if (data === null || data === undefined) { - return data; - } - - let filteredData = data; - let filteredFullData = fullData; - - const entityChecker = this.getEntityChecker(model, 'read'); - if (entityChecker) { - if (Array.isArray(data)) { - filteredData = []; - filteredFullData = []; - for (const [entityData, entityFullData] of zip(data, fullData)) { - if (!entityChecker.func(entityData, { user: this.user })) { - if (this.shouldLogQuery) { - this.logger.info( - `[policy] dropping ${model} entity${path ? ' at ' + path : ''} due to entity checker` - ); - } - } else { - filteredData.push(entityData); - filteredFullData.push(entityFullData); - } - } - } else { - if (!entityChecker.func(data, { user: this.user })) { - if (this.shouldLogQuery) { - this.logger.info( - `[policy] dropping ${model} entity${path ? ' at ' + path : ''} due to entity checker` - ); - } - return null; - } - } - } - - for (const [entityData, entityFullData] of zip(filteredData, filteredFullData)) { - if (typeof entityData !== 'object' || !entityData) { - continue; - } - - for (const [field, fieldData] of Object.entries(entityData)) { - if (fieldData === undefined) { - continue; - } - - const fieldInfo = resolveField(this.modelMeta, model, field); - if (!fieldInfo) { - // could be _count, etc. - continue; - } - - if (queryArgs?.omit && typeof queryArgs.omit === 'object' && queryArgs.omit[field] === true) { - // respect `{ omit: { [field]: true } }` - delete entityData[field]; - continue; - } - - if (queryArgs?.select && typeof queryArgs.select === 'object' && !queryArgs.select[field]) { - // respect select - delete entityData[field]; - continue; - } - - if ( - fieldInfo.isDataModel && - queryArgs?.include && - typeof queryArgs.include === 'object' && - !queryArgs.include[field] - ) { - // respect include - delete entityData[field]; - continue; - } - - if (hasFieldLevelPolicy) { - // 1. remove fields selected for checking field-level policies but not selected by the original query args - // 2. evaluate field-level policies and remove fields that are not readable - - if (!fieldInfo.isDataModel) { - // scalar field, delete unselected ones - const select = queryArgs?.select; - if (select && typeof select === 'object' && select[field] !== true) { - // there's a select clause but this field is not included - delete entityData[field]; - continue; - } - } else { - // relation field, delete if not selected or included - const include = queryArgs?.include; - const select = queryArgs?.select; - if (!include?.[field] && !select?.[field]) { - // relation field not included or selected - delete entityData[field]; - continue; - } - } - - // delete unreadable fields - if (!this.checkReadField(model, field, entityFullData)) { - if (this.shouldLogQuery) { - this.logger.info(`[policy] dropping unreadable field ${path ? path + '.' : ''}${field}`); - } - delete entityData[field]; - continue; - } - } - - if (fieldInfo.isDataModel) { - // recurse into nested fields - const nextArgs = (queryArgs?.select ?? queryArgs?.include)?.[field]; - const nestedResult = this.doPostProcessForRead( - fieldData, - fieldInfo.type, - entityFullData[field], - nextArgs, - this.hasFieldLevelPolicy(fieldInfo.type), - path ? path + '.' + field : field - ); - if (nestedResult === undefined) { - delete entityData[field]; - } else { - entityData[field] = nestedResult; - } - } - } - } - - return filteredData; - } - - /** - * Replace content of `target` object with `withObject` in-place. - */ - replace(target: any, withObject: any) { - if (!target || typeof target !== 'object' || !withObject || typeof withObject !== 'object') { - return; - } - - // remove missing keys - for (const key of Object.keys(target)) { - if (!(key in withObject)) { - delete target[key]; - } - } - - // overwrite keys - for (const [key, value] of Object.entries(withObject)) { - target[key] = value; - } - } - - /** - * Picks properties from an object. - */ - pick(value: T, ...props: (keyof T)[]): Pick { - const v: any = value; - return props.reduce(function (result, prop) { - if (prop in v) { - result[prop] = v[prop]; - } - return result; - }, {} as any); - } - - private mergeWhereClause(where: any, extra: any) { - if (!where) { - throw new Error('invalid where clause'); - } - - if (this.isTrue(extra)) { - return where; - } - - if (this.isFalse(extra)) { - return this.makeFalse(); - } - - // instead of simply wrapping with AND, we preserve the structure - // of the original where clause and merge `extra` into it so that - // unique query can continue working - if (where.AND) { - // merge into existing AND clause - const conditions = Array.isArray(where.AND) ? [...where.AND] : [where.AND]; - conditions.push(extra); - const combined: any = this.and(...conditions); - - // make sure the merging always goes under AND - return { ...where, AND: combined.AND ?? combined }; - } else { - // insert an AND clause - return { ...where, AND: [extra] }; - } - } - - /** - * Given an entity data, returns an object only containing id fields. - */ - getIdFieldValues(model: string, data: any) { - if (!data) { - return undefined; - } - const idFields = this.getIdFields(model); - return Object.fromEntries(idFields.map((f) => [f.name, data[f.name]])); - } - - //#endregion -} diff --git a/packages/runtime/src/enhancements/node/promise.ts b/packages/runtime/src/enhancements/node/promise.ts deleted file mode 100644 index 471fcb642..000000000 --- a/packages/runtime/src/enhancements/node/promise.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { getModelInfo, type ModelMeta } from '../../cross'; - -/** - * 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 { - let promise: Promise | undefined; - const cb = () => { - try { - return (promise ??= valueToPromise(callback())); - } catch (err) { - // deal with synchronous errors - return Promise.reject(err); - } - }; - - return { - then(onFulfilled, onRejected) { - return cb().then(onFulfilled, onRejected); - }, - catch(onRejected) { - return cb().catch(onRejected); - }, - finally(onFinally) { - return cb().finally(onFinally); - }, - [Symbol.toStringTag]: 'ZenStackPromise', - }; -} - -function valueToPromise(thing: any): Promise { - if (typeof thing === 'object' && typeof thing?.then === 'function') { - return thing; - } else { - return Promise.resolve(thing); - } -} - -/** - * Create a deferred promise with fluent API call stub installed. - * - * @param callback The callback to execute when the promise is awaited. - * @param parentArgs The parent promise's query args. - * @param modelMeta The model metadata. - * @param model The model name. - */ -export function createFluentPromise( - callback: () => Promise, - parentArgs: any, - modelMeta: ModelMeta, - model: string -): Promise { - const promise: any = createDeferredPromise(callback); - - const modelInfo = getModelInfo(modelMeta, model); - if (!modelInfo) { - return promise; - } - - // install fluent call stub for model fields - Object.values(modelInfo.fields) - .filter((field) => field.isDataModel) - .forEach((field) => { - // e.g., `posts` in `db.user.findUnique(...).posts()` - promise[field.name] = (fluentArgs: any) => { - if (field.isArray) { - // an array relation terminates fluent call chain - return createDeferredPromise(async () => { - setFluentSelect(parentArgs, field.name, fluentArgs ?? true); - const parentResult: any = await promise; - return parentResult?.[field.name] ?? null; - }); - } else { - fluentArgs = { ...fluentArgs }; - // create a chained subsequent fluent call promise - return createFluentPromise( - async () => { - setFluentSelect(parentArgs, field.name, fluentArgs); - const parentResult: any = await promise; - return parentResult?.[field.name] ?? null; - }, - fluentArgs, - modelMeta, - field.type - ); - } - }; - }); - - return promise; -} - -function setFluentSelect(args: any, fluentFieldName: any, fluentArgs: any) { - delete args.include; - args.select = { [fluentFieldName]: fluentArgs }; -} diff --git a/packages/runtime/src/enhancements/node/proxy.ts b/packages/runtime/src/enhancements/node/proxy.ts deleted file mode 100644 index c940c9af6..000000000 --- a/packages/runtime/src/enhancements/node/proxy.ts +++ /dev/null @@ -1,394 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { PRISMA_PROXY_ENHANCER } from '../../constants'; -import { type ModelMeta, clone } from '../../cross'; -import type { DbClientContract, ErrorTransformer } from '../../types'; -import type { InternalEnhancementOptions } from './create-enhancement'; -import { createDeferredPromise, createFluentPromise } from './promise'; - -/** - * Prisma batch write operation result - */ -export type BatchResult = { count: number }; - -/** - * Interface for proxy that intercepts Prisma operations. - */ -export interface PrismaProxyHandler { - findUnique(args: any): Promise; - - findUniqueOrThrow(args: any): Promise; - - findFirst(args: any): Promise; - - findFirstOrThrow(args: any): Promise; - - findMany(args: any): Promise; - - create(args: any): Promise; - - createMany(args: { data: any; skipDuplicates?: boolean }): Promise; - - createManyAndReturn(args: { data: any; select?: any; skipDuplicates?: boolean }): Promise; - - update(args: any): Promise; - - updateMany(args: any): Promise; - - updateManyAndReturn(args: any): Promise; - - upsert(args: any): Promise; - - delete(args: any): Promise; - - deleteMany(args: any): Promise; - - aggregate(args: any): Promise; - - groupBy(args: any): Promise; - - count(args: any): Promise; - - subscribe(args: any): Promise; - - stream(args: any): Promise; -} - -/** - * All Prisma operation names - */ -export type PrismaProxyActions = keyof PrismaProxyHandler; - -/** - * A default implementation of @see PrismaProxyHandler which directly - * delegates to the wrapped Prisma client. It offers a few overridable - * methods to allow more easily inject custom logic. - */ -export class DefaultPrismaProxyHandler implements PrismaProxyHandler { - constructor( - protected readonly prisma: DbClientContract, - protected readonly model: string, - protected readonly options: InternalEnhancementOptions - ) {} - - protected withFluentCall(method: PrismaProxyActions, args: any, postProcess = true): Promise { - args = args ? clone(args) : {}; - const promise = createFluentPromise( - async () => { - args = await this.preprocessArgs(method, args); - const r = await this.prisma[this.model][method](args); - return postProcess ? this.processResultEntity(method, r) : r; - }, - args, - this.options.modelMeta, - this.model - ); - return promise; - } - - protected deferred(method: PrismaProxyActions, args: any, postProcess = true) { - return createDeferredPromise(async () => { - args = await this.preprocessArgs(method, args); - const r = await this.prisma[this.model][method](args); - return postProcess ? this.processResultEntity(method, r) : r; - }); - } - - findUnique(args: any) { - return this.withFluentCall('findUnique', args); - } - - findUniqueOrThrow(args: any) { - return this.withFluentCall('findUniqueOrThrow', args); - } - - findFirst(args: any) { - return this.withFluentCall('findFirst', args); - } - - findFirstOrThrow(args: any) { - return this.withFluentCall('findFirstOrThrow', args); - } - - findMany(args: any) { - return this.deferred('findMany', args); - } - - create(args: any): Promise { - return this.deferred('create', args); - } - - createMany(args: { data: any; skipDuplicates?: boolean }) { - return this.deferred<{ count: number }>('createMany', args, false); - } - - createManyAndReturn(args: { data: any; select?: any; skipDuplicates?: boolean }) { - return this.deferred('createManyAndReturn', args); - } - - update(args: any) { - return this.deferred('update', args); - } - - updateMany(args: any) { - return this.deferred<{ count: number }>('updateMany', args, false); - } - - updateManyAndReturn(args: any) { - return this.deferred('updateManyAndReturn', args); - } - - upsert(args: any) { - return this.deferred('upsert', args); - } - - delete(args: any) { - return this.deferred('delete', args); - } - - deleteMany(args: any) { - return this.deferred<{ count: number }>('deleteMany', args, false); - } - - aggregate(args: any) { - return this.deferred('aggregate', args, false); - } - - groupBy(args: any) { - return this.deferred('groupBy', args, false); - } - - count(args: any): Promise { - return this.deferred('count', args, false); - } - - subscribe(args: any) { - return this.doSubscribeStream('subscribe', args); - } - - stream(args: any) { - return this.doSubscribeStream('stream', args); - } - - private async doSubscribeStream(method: 'subscribe' | 'stream', args: any) { - // Prisma's `subscribe` and `stream` methods return an async iterable - // which we need to wrap to process the iteration results - const iterable = await this.prisma[this.model][method](args); - return { - [Symbol.asyncIterator]: () => { - const iter = iterable[Symbol.asyncIterator].bind(iterable)(); - return { - next: async () => { - const { done, value } = await iter.next(); - const processedValue = value ? await this.processResultEntity(method, value) : value; - return { done, value: processedValue }; - }, - return: () => iter.return?.(), - throw: () => iter.throw?.(), - }; - }, - }; - } - - /** - * Processes result entities before they're returned - */ - protected async processResultEntity(_method: PrismaProxyActions, data: T): Promise { - return data; - } - - /** - * Processes query args before they're passed to Prisma. - */ - protected async preprocessArgs(_method: PrismaProxyActions, args: any) { - return args; - } -} - -// a marker for filtering error stack trace -const ERROR_MARKER = '__error_marker__'; - -const customInspect = Symbol.for('nodejs.util.inspect.custom'); - -/** - * Makes a Prisma client proxy. - */ -export function makeProxy( - prisma: any, - modelMeta: ModelMeta, - makeHandler: (prisma: object, model: string) => T, - name = 'unnamed_enhancer', - errorTransformer?: ErrorTransformer -) { - const models = Object.keys(modelMeta.models).map((k) => k.toLowerCase()); - - const proxy = new Proxy(prisma, { - get: (target: any, prop: string | symbol, receiver: any) => { - // enhancer metadata - if (prop === PRISMA_PROXY_ENHANCER) { - return name; - } - - if (prop === '$transaction') { - // for interactive transactions, we need to proxy the transaction function so that - // when it runs the callback, it provides a proxy to the Prisma client wrapped with - // the same handler - // - // TODO: batch transaction is not supported yet, how? - const $transaction = Reflect.get(target, prop, receiver); - if ($transaction) { - return (input: any, ...rest: any[]) => { - if (Array.isArray(input)) { - throw new Error( - 'Sequential operations transaction is not supported by ZenStack enhanced Prisma client. Please use interactive transaction instead.' - ); - } else if (typeof input !== 'function') { - throw new Error('A function value input is expected'); - } - - const txFunc = input; - return $transaction.bind(target)((tx: any) => { - // create a proxy for the transaction function - const txProxy = makeProxy(tx, modelMeta, makeHandler, name + '$tx'); - - // call the transaction function with the proxy - return txFunc(txProxy); - }, ...rest); - }; - } else { - return $transaction; - } - } - - if (prop === '$extends') { - // Prisma's `$extends` API returns a new client instance, we need to recreate - // a proxy around it - const $extends = Reflect.get(target, prop, receiver); - if ($extends && typeof $extends === 'function') { - return (...args: any[]) => { - const result = $extends.bind(target)(...args); - return makeProxy(result, modelMeta, makeHandler, name + '$ext', errorTransformer); - }; - } else { - return $extends; - } - } - - if (typeof prop !== 'string' || prop.startsWith('$') || !models.includes(prop.toLowerCase())) { - // skip non-model fields - return Reflect.get(target, prop, receiver); - } - - const propVal = Reflect.get(target, prop, receiver); - if (!propVal || typeof propVal !== 'object') { - return propVal; - } - - return createHandlerProxy(makeHandler(target, prop), propVal, prop, proxy, errorTransformer); - }, - }); - - proxy[customInspect] = `$zenstack_prisma_${prisma._clientVersion}`; - - return proxy; -} - -// A proxy for capturing errors and processing stack trace -function createHandlerProxy( - handler: T, - origTarget: any, - model: string, - dbOrTx: any, - errorTransformer?: ErrorTransformer -): T { - return new Proxy(handler, { - get(target, propKey) { - if (propKey === '$parent') { - return dbOrTx; - } - - const prop = target[propKey as keyof T]; - if (typeof prop !== 'function') { - // the proxy handler doesn't have this method, fall back to the original target - // this can happen for new methods added by Prisma Client Extensions - return origTarget[propKey]; - } - - // eslint-disable-next-line @typescript-eslint/ban-types - const origMethod = prop as Function; - return function (...args: any[]) { - // using proxy with async functions results in messed-up error stack trace, - // create an error to capture the current stack - const capture = new Error(ERROR_MARKER); - - // the original proxy returned by the PrismaClient proxy - const promise: Promise = origMethod.apply(handler, args); - - // modify the error stack - const resultPromise = createDeferredPromise(() => { - return new Promise((resolve, reject) => { - promise.then( - (value) => resolve(value), - (err) => { - if (capture.stack && err instanceof Error) { - // save the original stack and replace it with a clean one - (err as any).internalStack = err.stack; - err.stack = cleanCallStack(capture.stack, model, propKey.toString(), err.message); - } - - if (errorTransformer) { - err = errorTransformer ? errorTransformer(err) : err; - } - reject(err); - } - ); - }); - }); - - // carry over extra fields from the original promise - for (const [k, v] of Object.entries(promise)) { - if (!(k in resultPromise)) { - (resultPromise as any)[k] = v; - } - } - - return resultPromise; - }; - }, - }); -} - -// Filter out @zenstackhq/runtime stack (generated by proxy) from stack trace -function cleanCallStack(stack: string, model: string, method: string, message: string) { - // message line - let resultStack = `Error calling enhanced Prisma method \`${model}.${method}\`: ${message}`; - - const lines = stack.split('\n'); - let foundMarker = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (!foundMarker) { - // find marker, then stack trace lines follow - if (line.includes(ERROR_MARKER)) { - foundMarker = true; - } - continue; - } - - // skip leading zenstack and anonymous lines - if (line.includes('@zenstackhq/runtime') || line.includes('Proxy.')) { - continue; - } - - // capture remaining lines - resultStack += lines - .slice(i) - .map((l) => '\n' + l) - .join(); - break; - } - - return resultStack; -} diff --git a/packages/runtime/src/enhancements/node/query-utils.ts b/packages/runtime/src/enhancements/node/query-utils.ts deleted file mode 100644 index 58cd0098d..000000000 --- a/packages/runtime/src/enhancements/node/query-utils.ts +++ /dev/null @@ -1,297 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - clone, - getIdFields, - getModelInfo, - getUniqueConstraints, - resolveField, - type FieldInfo, - type NestedWriteVisitorContext, -} from '../../cross'; -import type { CrudContract, DbClientContract } from '../../types'; -import { getVersion } from '../../version'; -import { formatObject } from '../edge'; -import { InternalEnhancementOptions } from './create-enhancement'; -import { Logger } from './logger'; -import { prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; - -export class QueryUtils { - protected readonly logger: Logger; - - constructor(private readonly prisma: DbClientContract, protected readonly options: InternalEnhancementOptions) { - this.logger = new Logger(prisma); - } - - getIdFields(model: string) { - return getIdFields(this.options.modelMeta, model, true); - } - - makeIdSelection(model: string) { - const idFields = this.getIdFields(model); - return Object.assign({}, ...idFields.map((f) => ({ [f.name]: true }))); - } - - getEntityIds(model: string, entityData: any) { - const idFields = this.getIdFields(model); - const result: Record = {}; - for (const idField of idFields) { - result[idField.name] = entityData[idField.name]; - } - return result; - } - - /** - * Initiates a transaction. - */ - transaction(db: CrudContract, action: (tx: CrudContract) => Promise) { - const fullDb = db as DbClientContract; - if (fullDb['$transaction']) { - return fullDb.$transaction( - (tx) => { - (tx as any)[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient$tx'; - return action(tx); - }, - { - maxWait: this.options.transactionMaxWait, - timeout: this.options.transactionTimeout, - isolationLevel: this.options.transactionIsolationLevel, - } - ); - } else { - // already in transaction, don't nest - return action(db); - } - } - - /** - * Builds a reversed query for the given nested path. - */ - async buildReversedQuery( - db: CrudContract, - context: NestedWriteVisitorContext, - forMutationPayload = false, - uncheckedOperation = false - ) { - let result, currQuery: any; - let currField: FieldInfo | undefined; - - for (let i = context.nestingPath.length - 1; i >= 0; i--) { - const { field, model, where } = context.nestingPath[i]; - - // never modify the original where because it's shared in the structure - const visitWhere = { ...where }; - if (model && where) { - // make sure composite unique condition is flattened - this.flattenGeneratedUniqueField(model, visitWhere); - } - - if (!result) { - // first segment (bottom), just use its where clause - result = currQuery = { ...visitWhere }; - currField = field; - } else { - if (!currField) { - throw this.unknownError(`missing field in nested path`); - } - if (!currField.backLink) { - throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`); - } - - const backLinkField = this.getModelField(currField.type, currField.backLink); - if (!backLinkField) { - throw this.unknownError(`missing backLink field ${currField.backLink} in ${currField.type}`); - } - - if (backLinkField.isArray && !forMutationPayload) { - // many-side of relationship, wrap with "some" query - currQuery[currField.backLink] = { some: { ...visitWhere } }; - currQuery = currQuery[currField.backLink].some; - } else { - const fkMapping = where && backLinkField.isRelationOwner && backLinkField.foreignKeyMapping; - - // calculate if we should preserve the relation condition (e.g., { user: { id: 1 } }) - const shouldPreserveRelationCondition = - // doing a mutation - forMutationPayload && - // and it's not an unchecked mutate - !uncheckedOperation && - // and the current segment is the direct parent (the last one is the mutate itself), - // the relation condition should be preserved and will be converted to a "connect" later - i === context.nestingPath.length - 2; - - if (fkMapping && !shouldPreserveRelationCondition) { - // turn relation condition into foreign key condition, e.g.: - // { user: { id: 1 } } => { userId: 1 } - - let parentPk = visitWhere; - if (Object.keys(fkMapping).some((k) => !(k in parentPk) || parentPk[k] === undefined)) { - // it can happen that the parent condition actually doesn't contain all id fields - // (when the parent condition is not a primary key but unique constraints) - // and in such case we need to load it to get the pks - - if (this.options.logPrismaQuery && this.logger.enabled('info')) { - this.logger.info( - `[reverseLookup] \`findUniqueOrThrow\` ${model}: ${formatObject(where)}` - ); - } - parentPk = await db[model].findUniqueOrThrow({ - where, - select: this.makeIdSelection(model), - }); - } - - for (const [r, fk] of Object.entries(fkMapping)) { - currQuery[fk] = parentPk[r]; - } - - if (i > 0) { - // prepare for the next segment - currQuery[currField.backLink] = {}; - } - } else { - // preserve the original structure - currQuery[currField.backLink] = { ...visitWhere }; - } - - if (forMutationPayload && currQuery[currField.backLink]) { - // reconstruct compound unique field - currQuery[currField.backLink] = this.composeCompoundUniqueField( - backLinkField.type, - currQuery[currField.backLink] - ); - } - - currQuery = currQuery[currField.backLink]; - } - currField = field; - } - } - return result; - } - - /** - * Composes a compound unique field from multiple fields. E.g.: { a: '1', b: '1' } => { a_b: { a: '1', b: '1' } }. - */ - composeCompoundUniqueField(model: string, fieldData: any) { - const uniqueConstraints = getUniqueConstraints(this.options.modelMeta, model); - if (!uniqueConstraints) { - return fieldData; - } - - const result: any = this.safeClone(fieldData); - for (const [name, constraint] of Object.entries(uniqueConstraints)) { - if (constraint.fields.length > 1 && constraint.fields.every((f) => fieldData[f] !== undefined)) { - // multi-field unique constraint, compose it - result[name] = constraint.fields.reduce( - (prev, field) => ({ ...prev, [field]: fieldData[field] }), - {} - ); - constraint.fields.forEach((f) => delete result[f]); - } - } - return result; - } - - /** - * Flattens a generated unique field. E.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }. - */ - flattenGeneratedUniqueField(model: string, args: any) { - const uniqueConstraints = getUniqueConstraints(this.options.modelMeta, model); - if (uniqueConstraints && Object.keys(uniqueConstraints).length > 0) { - for (const [field, value] of Object.entries(args)) { - if ( - uniqueConstraints[field] && - uniqueConstraints[field].fields.length > 1 && - typeof value === 'object' - ) { - // multi-field unique constraint, flatten it - delete args[field]; - if (value) { - for (const [f, v] of Object.entries(value)) { - args[f] = v; - } - } - } - } - } - } - - validationError(message: string) { - return prismaClientValidationError(this.prisma, this.options.prismaModule, message); - } - - unknownError(message: string) { - return prismaClientUnknownRequestError(this.prisma, this.options.prismaModule, message, { - clientVersion: getVersion(), - }); - } - - getModelFields(model: string) { - return getModelInfo(this.options.modelMeta, model)?.fields; - } - - /** - * Gets information for a specific model field. - */ - getModelField(model: string, field: string) { - return resolveField(this.options.modelMeta, model, field); - } - - /** - * Clones an object and makes sure it's not empty. - */ - safeClone(value: unknown): any { - return value ? clone(value) : value === undefined || value === null ? {} : value; - } - - getDelegateConcreteModel(model: string, data: any) { - if (!data || typeof data !== 'object') { - return model; - } - - const modelInfo = getModelInfo(this.options.modelMeta, model); - if (modelInfo?.discriminator) { - // model has a discriminator so it can be a polymorphic base, - // need to find the concrete model - const concreteModelName = data[modelInfo.discriminator]; - if (typeof concreteModelName === 'string' && concreteModelName) { - return concreteModelName; - } - } - - return model; - } - - /** - * Gets relation info for a foreign key field. - */ - getRelationForForeignKey(model: string, fkField: string) { - const modelInfo = getModelInfo(this.options.modelMeta, model); - if (!modelInfo) { - return undefined; - } - - for (const field of Object.values(modelInfo.fields)) { - if (field.foreignKeyMapping) { - const entry = Object.entries(field.foreignKeyMapping).find(([, v]) => v === fkField); - if (entry) { - return { relation: field, idField: entry[0], fkField: entry[1] }; - } - } - } - - return undefined; - } - - /** - * Gets fields of object with defined values. - */ - getFieldsWithDefinedValues(data: object) { - if (!data) { - return []; - } - return Object.entries(data) - .filter(([, v]) => v !== undefined) - .map(([k]) => k); - } -} diff --git a/packages/runtime/src/enhancements/node/types.ts b/packages/runtime/src/enhancements/node/types.ts deleted file mode 100644 index c9a90baa8..000000000 --- a/packages/runtime/src/enhancements/node/types.ts +++ /dev/null @@ -1,282 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { CrudContract, PermissionCheckerContext, PolicyCrudKind, QueryContext } from '../../types'; - -/** - * Common options for PrismaClient enhancements - */ -export interface CommonEnhancementOptions { - /** - * Path for loading CLI-generated code - */ - loadPath?: string; - - /** - * The `Prisma` module generated together with `PrismaClient`. You only need to - * pass it when you specified a custom `PrismaClient` output path. The module can - * be loaded like: `import { Prisma } from '';`. - */ - prismaModule?: any; -} - -/** - * CRUD operations - */ -export type CRUD = 'create' | 'read' | 'update' | 'delete'; - -/** - * Function for getting policy guard with a given context - */ -export type PolicyFunc = (context: QueryContext, db: CrudContract) => object; - -/** - * Function for checking an entity's data for permission - */ -export type EntityCheckerFunc = (input: any, context: QueryContext) => boolean; - -/** - * Function for checking if an operation is possibly allowed. - */ -export type PermissionCheckerFunc = (context: PermissionCheckerContext) => PermissionCheckerConstraint; - -/** - * Supported checker constraint checking value types. - */ -export type ConstraintValueTypes = 'boolean' | 'number' | 'string'; - -/** - * Free variable constraint - */ -export type VariableConstraint = { kind: 'variable'; name: string; type: ConstraintValueTypes }; - -/** - * Constant value constraint - */ -export type ValueConstraint = { - kind: 'value'; - value: number | boolean | string; - type: ConstraintValueTypes; -}; - -/** - * Terms for comparison constraints - */ -export type ComparisonTerm = VariableConstraint | ValueConstraint; - -/** - * Comparison constraint - */ -export type ComparisonConstraint = { - kind: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; - left: ComparisonTerm; - right: ComparisonTerm; -}; - -/** - * Logical constraint - */ -export type LogicalConstraint = { - kind: 'and' | 'or' | 'not'; - children: PermissionCheckerConstraint[]; -}; - -/** - * Constraint delegated to another model through `check()` function call - * on a relation field. - */ -export type DelegateConstraint = { - kind: 'delegate'; - model: string; - relation: string; - operation?: CRUD; -}; - -/** - * Operation allowability checking constraint - */ -export type PermissionCheckerConstraint = - | ValueConstraint - | VariableConstraint - | ComparisonConstraint - | LogicalConstraint - | DelegateConstraint; - -/** - * Policy definition - */ -export type PolicyDef = { - // policy definitions for each model - policy: Record; - - // tracks which models have data validation rules - validation: Record; - - // a { select: ... } object for fetching `auth()` fields needed for policy evaluation - authSelector?: object; -}; - -type ModelName = string; -type FieldName = string; - -/** - * Policy definition for a model - */ -export type ModelPolicyDef = { - /** - * Model-level CRUD policies - */ - modelLevel: ModelCrudDef; - - /** - * Field-level CRUD policies - */ - fieldLevel?: FieldCrudDef; -}; - -/** - * CRUD policy definitions for a model - */ -export type ModelCrudDef = { - read: ModelReadDef; - create: ModelCreateDef; - update: ModelUpdateDef; - delete: ModelDeleteDef; - postUpdate: ModelPostUpdateDef; -}; - -/** - * Information for checking entity data outside of Prisma - */ -export type EntityChecker = { - /** - * Checker function - */ - func: EntityCheckerFunc; - - /** - * Selector for fetching entity data - */ - selector?: object; -}; - -/** - * Common policy definition for a CRUD operation - */ -type ModelCrudCommon = { - /** - * Prisma query guard or a constant condition - */ - guard: PolicyFunc | boolean; - - /** - * Additional checker function for checking policies outside of Prisma - */ - /** - * Additional checker function for checking policies outside of Prisma - */ - entityChecker?: EntityChecker; - - /** - * Permission checker function or a constant condition - */ - permissionChecker?: PermissionCheckerFunc | boolean; -}; - -/** - * Policy definition for reading a model - */ -type ModelReadDef = ModelCrudCommon; - -/** - * Policy definition for creating a model - */ -type ModelCreateDef = ModelCrudCommon & { - /** - * Create input validation function. Only generated when a create - * can be approved or denied based on input values. - */ - inputChecker?: EntityCheckerFunc | boolean; -}; - -/** - * Policy definition for updating a model - */ -type ModelUpdateDef = ModelCrudCommon; - -/** - * Policy definition for deleting a model - */ -type ModelDeleteDef = ModelCrudCommon; - -/** - * Policy definition for post-update checking a model - */ -type ModelPostUpdateDef = Exclude & { - preUpdateSelector?: object; -}; - -/** - * CRUD policy definitions for a field - */ -type FieldCrudDef = { - /** - * Field-level read policy - */ - read: Record; - - /** - * Field-level update policy - */ - update: Record; -}; - -type FieldReadDef = { - /** - * Field-level Prisma query guard - */ - guard?: PolicyFunc; - - /** - * Entity checker - */ - entityChecker?: EntityChecker; - - /** - * Field-level read override Prisma query guard - */ - overrideGuard?: PolicyFunc; - - /** - * Entity checker for override policies - */ - overrideEntityChecker?: EntityChecker; -}; - -type FieldUpdateDef = { - /** - * Field-level update Prisma query guard - */ - guard?: PolicyFunc; - - /** - * Additional entity checker - */ - entityChecker?: EntityChecker; - - /** - * Field-level update override Prisma query guard - */ - overrideGuard?: PolicyFunc; - - /** - * Additional entity checker for override policies - */ - overrideEntityChecker?: EntityChecker; -}; - -/** - * Permission check API (`check()`) arguments - */ -export type PermissionCheckArgs = { - operation: PolicyCrudKind; - where?: Record; -}; diff --git a/packages/runtime/src/enhancements/node/utils.ts b/packages/runtime/src/enhancements/node/utils.ts deleted file mode 100644 index 05d906046..000000000 --- a/packages/runtime/src/enhancements/node/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import safeJsonStringify from 'safe-json-stringify'; -import { resolveField, type FieldInfo, type ModelMeta } from '../../cross'; -import type { DbClientContract } from '../../types'; - -/** - * Formats an object for pretty printing. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function formatObject(value: any, multiLine = true) { - return safeJsonStringify(value, (_, v) => (typeof v === 'bigint' ? v.toString() : v), multiLine ? 2 : undefined); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function prismaClientValidationError(prisma: DbClientContract, prismaModule: any, message: string): Error { - throw new prismaModule.PrismaClientValidationError(message, { clientVersion: prisma._clientVersion }); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function prismaClientKnownRequestError(prisma: DbClientContract, prismaModule: any, ...args: unknown[]): Error { - return new prismaModule.PrismaClientKnownRequestError(...args); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function prismaClientUnknownRequestError(prismaModule: any, ...args: unknown[]): Error { - throw new prismaModule.PrismaClientUnknownRequestError(...args); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isUnsafeMutate(model: string, args: any, modelMeta: ModelMeta) { - if (!args) { - return false; - } - for (const k of Object.keys(args)) { - const field = resolveField(modelMeta, model, k); - if (field && (isAutoIncrementIdField(field) || field.isForeignKey)) { - return true; - } - } - return false; -} - -export function isAutoIncrementIdField(field: FieldInfo) { - return field.isId && field.isAutoIncrement; -} diff --git a/packages/runtime/src/enhancements/node/where-visitor.ts b/packages/runtime/src/enhancements/node/where-visitor.ts deleted file mode 100644 index 71b303985..000000000 --- a/packages/runtime/src/enhancements/node/where-visitor.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { enumerate, resolveField, type FieldInfo, type ModelMeta } from '../../cross'; - -/** - * Context for visiting - */ -export type WhereVisitorContext = { - /** - * Parent data, can be used to replace fields - */ - parent: any; - - /** - * Current field, undefined if toplevel - */ - field?: FieldInfo; -}; - -/** - * WhereVisitor's callback actions - */ -export type WhereVisitorCallback = { - field?: (field: FieldInfo, data: any) => Promise; -}; - -const FILTER_OPERATORS = [ - 'equals', - 'not', - 'in', - 'notIn', - 'lt', - 'lte', - 'gt', - 'gte', - 'contains', - 'search', - 'startsWith', - 'endsWith', -]; - -const RELATION_FILTER_OPERATORS = ['is', 'isNot', 'some', 'every', 'none']; - -/** - * Recursive visitor for where payload - */ -export class WhereVisitor { - constructor(private readonly modelMeta: ModelMeta, private readonly callback: WhereVisitorCallback) {} - - /** - * Start visiting - */ - async visit(model: string, where: any): Promise { - if (!where) { - return; - } - - for (const [k, v] of Object.entries(where)) { - if (['AND', 'OR', 'NOT'].includes(k)) { - for (const item of enumerate(v)) { - await this.visit(model, item); - } - continue; - } - - if (RELATION_FILTER_OPERATORS.includes(k)) { - // visit into filter body - await this.visit(model, v); - continue; - } - - const field = resolveField(this.modelMeta, model, k); - if (!field) { - continue; - } - - if (typeof v === 'object') { - const filterOp = Object.keys(v).find((f) => FILTER_OPERATORS.includes(f)); - if (filterOp) { - // reach into filter value - const newValue = await this.callback.field?.(field, v[filterOp]); - v[filterOp] = newValue; - continue; - } - - if (Object.keys(v).some((f) => RELATION_FILTER_OPERATORS.includes(f))) { - // filter payload - await this.visit(field.type, v); - continue; - } - } - - // scalar field - const newValue = await this.callback.field?.(field, v); - where[k] = newValue; - } - } -} diff --git a/packages/runtime/src/error.ts b/packages/runtime/src/error.ts deleted file mode 100644 index 22422e7b8..000000000 --- a/packages/runtime/src/error.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export function isPrismaClientKnownRequestError( - err: any -): err is { code: string; message: string; meta?: Record } { - return findConstructorName(err.__proto__, 'PrismaClientKnownRequestError'); -} - -export function isPrismaClientUnknownRequestError(err: any): err is { message: string } { - return findConstructorName(err.__proto__, 'PrismaClientUnknownRequestError'); -} - -export function isPrismaClientValidationError(err: any): err is { message: string } { - return findConstructorName(err.__proto__, 'PrismaClientValidationError'); -} - -function findConstructorName(proto: any, name: string): boolean { - if (!proto) { - return false; - } - return proto.constructor.name === name || findConstructorName(proto.__proto__, name); -} diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts deleted file mode 100644 index a735da875..000000000 --- a/packages/runtime/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './constants'; -export * from './cross'; -export * from './enhance'; -export * from './error'; -export * from './types'; -export * from './validation'; -export * from './version'; diff --git a/packages/runtime/src/local-helpers/index.ts b/packages/runtime/src/local-helpers/index.ts deleted file mode 100644 index a12c5b8b6..000000000 --- a/packages/runtime/src/local-helpers/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './is-plain-object'; -export * from './lower-case-first'; -export * from './param-case'; -export * from './simple-traverse'; -export * from './sleep'; -export * from './tiny-invariant'; -export * from './upper-case-first'; -export * from './zod-utils'; diff --git a/packages/runtime/src/local-helpers/is-plain-object.ts b/packages/runtime/src/local-helpers/is-plain-object.ts deleted file mode 100644 index f5c13d6df..000000000 --- a/packages/runtime/src/local-helpers/is-plain-object.ts +++ /dev/null @@ -1,23 +0,0 @@ -function isObject(o: unknown) { - return Object.prototype.toString.call(o) === '[object Object]'; -} - -export function isPlainObject(o: unknown) { - if (isObject(o) === false) return false; - - // If has modified constructor - const ctor = (o as { constructor: unknown }).constructor; - if (ctor === undefined) return true; - - // If has modified prototype - const prot = (ctor as { prototype: unknown }).prototype; - if (isObject(prot) === false) return false; - - // If constructor does not have an Object-specific method - if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) { - return false; - } - - // Most likely a plain Object - return true; -} diff --git a/packages/runtime/src/local-helpers/lower-case-first.ts b/packages/runtime/src/local-helpers/lower-case-first.ts deleted file mode 100644 index a05a05be1..000000000 --- a/packages/runtime/src/local-helpers/lower-case-first.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function lowerCaseFirst(input: string) { - return input.charAt(0).toLowerCase() + input.slice(1); -} diff --git a/packages/runtime/src/local-helpers/param-case.ts b/packages/runtime/src/local-helpers/param-case.ts deleted file mode 100644 index 3cb1f0179..000000000 --- a/packages/runtime/src/local-helpers/param-case.ts +++ /dev/null @@ -1,18 +0,0 @@ -const DEFAULT_SPLIT_REGEXP_1 = /([a-z0-9])([A-Z])/g; -const DEFAULT_SPLIT_REGEXP_2 = /([A-Z])([A-Z][a-z])/g; -const DEFAULT_STRIP_REGEXP = /[^A-Z0-9]+/gi; - -export function paramCase(input: string) { - const result = input - .replace(DEFAULT_SPLIT_REGEXP_1, "$1\0$2") - .replace(DEFAULT_SPLIT_REGEXP_2, "$1\0$2") - .replace(DEFAULT_STRIP_REGEXP, "\0"); - - let start = 0; - let end = result.length; - - while (result.charAt(start) === "\0") start++; - while (result.charAt(end - 1) === "\0") end--; - - return result.slice(start, end).split("\0").map((str) => str.toLowerCase()).join("-"); -} diff --git a/packages/runtime/src/local-helpers/simple-traverse.ts b/packages/runtime/src/local-helpers/simple-traverse.ts deleted file mode 100644 index 40ec68853..000000000 --- a/packages/runtime/src/local-helpers/simple-traverse.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -type Cb = ( - data: { - path: readonly string[]; - key: string | undefined; - value: any; - update: (nextValue: any) => void; - } -) => void; - -export function simpleTraverse(root: T, cb: Cb) { - const path: string[] = []; - const parents: any[] = []; - - function walker(node: any) { - const isObject = typeof node === 'object' && node !== null; - const isCircular = isObject && parents.some((p) => p === node); - - let keepGoing = true; - - function update(nextValue: any) { - if (path.length) { - const parent = parents[parents.length - 1]; - const key = path[path.length - 1]; - parent[key] = nextValue; - } - - node = nextValue; - - keepGoing = false; - } - - cb({ - path: [...path], - key: path[path.length - 1], - value: node, - update, - }); - - if (!keepGoing) return node; - - if (isObject && !isCircular) { - parents.push(node); - - Object.keys(node).forEach((key) => { - path.push(key); - - walker(node[key]); - - path.pop(); - }); - - parents.pop(); - } - - return node; - } - - return walker(root); -} diff --git a/packages/runtime/src/local-helpers/sleep.ts b/packages/runtime/src/local-helpers/sleep.ts deleted file mode 100644 index 5281ad0b6..000000000 --- a/packages/runtime/src/local-helpers/sleep.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function sleep(timeout: number) { - return new Promise((resolve) => { - setTimeout(() => resolve(), timeout); - }); -} diff --git a/packages/runtime/src/local-helpers/tiny-invariant.ts b/packages/runtime/src/local-helpers/tiny-invariant.ts deleted file mode 100644 index 5c0683a54..000000000 --- a/packages/runtime/src/local-helpers/tiny-invariant.ts +++ /dev/null @@ -1,17 +0,0 @@ -const isProduction = process.env.NODE_ENV === 'production'; -const prefix = 'Invariant failed'; - -export function invariant( - condition: unknown, - message?: string, -): asserts condition { - if (condition) { - return; - } - - if (isProduction) { - throw new Error(prefix); - } - - throw new Error(message ? `${prefix}: ${message}` : prefix); -} diff --git a/packages/runtime/src/local-helpers/upper-case-first.ts b/packages/runtime/src/local-helpers/upper-case-first.ts deleted file mode 100644 index 16a6a6e44..000000000 --- a/packages/runtime/src/local-helpers/upper-case-first.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function upperCaseFirst(input: string) { - return input.charAt(0).toUpperCase() + input.slice(1); -} diff --git a/packages/runtime/src/local-helpers/zod-utils.ts b/packages/runtime/src/local-helpers/zod-utils.ts deleted file mode 100644 index 01e6b9d66..000000000 --- a/packages/runtime/src/local-helpers/zod-utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { fromZodError as fromZodErrorV3 } from 'zod-validation-error/v3'; -import { fromZodError as fromZodErrorV4 } from 'zod-validation-error/v4'; - -/** - * Formats a Zod error message for better readability. Compatible with both Zod v3 and v4. - */ -export function getZodErrorMessage(err: unknown): string { - if (!(err instanceof Error)) { - return 'Unknown error'; - } - - try { - if ('_zod' in err) { - return fromZodErrorV4(err as any).message; - } else { - return fromZodErrorV3(err as any).message; - } - } catch { - return err.message; - } -} diff --git a/packages/runtime/src/package.json b/packages/runtime/src/package.json deleted file mode 120000 index 4e26811d4..000000000 --- a/packages/runtime/src/package.json +++ /dev/null @@ -1 +0,0 @@ -../package.json \ No newline at end of file diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts deleted file mode 100644 index fe584bb1b..000000000 --- a/packages/runtime/src/types.ts +++ /dev/null @@ -1,233 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import type { z } from 'zod'; -import { FieldInfo } from './cross'; - -export type PrismaPromise = Promise & Record PrismaPromise>; - -/** - * Weakly-typed database access methods - */ -export interface DbOperations { - findMany(args?: unknown): Promise; - findFirst(args?: unknown): PrismaPromise; - findFirstOrThrow(args?: unknown): PrismaPromise; - findUnique(args: unknown): PrismaPromise; - findUniqueOrThrow(args: unknown): PrismaPromise; - create(args: unknown): Promise; - createMany(args: unknown): Promise<{ count: number }>; - createManyAndReturn(args: unknown): Promise; - update(args: unknown): Promise; - updateMany(args: unknown): Promise<{ count: number }>; - updateManyAndReturn(args: unknown): Promise; - upsert(args: unknown): Promise; - delete(args: unknown): Promise; - deleteMany(args?: unknown): Promise<{ count: number }>; - aggregate(args: unknown): Promise; - groupBy(args: unknown): Promise; - count(args?: unknown): Promise; - subscribe(args?: unknown): Promise; - stream(args?: unknown): Promise; - check(args: unknown): Promise; - fields: Record; -} - -/** - * Kinds of access policy - */ -export type PolicyKind = 'allow' | 'deny'; - -export type PolicyCrudKind = 'read' | 'create' | 'update' | 'delete'; - -/** - * Kinds of operations controlled by access policies - */ -export type PolicyOperationKind = PolicyCrudKind | 'postUpdate'; - -/** - * Current login user info - */ -export type AuthUser = Record; - -/** - * Context for database query - */ -export type QueryContext = { - /** - * Current login user (provided by @see RequestHandlerOptions) - */ - user?: AuthUser; - - /** - * Pre-update value of the entity - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - preValue?: any; -}; - -/** - * Context for checking operation allowability. - */ -export type PermissionCheckerContext = { - /** - * Current user - */ - user?: AuthUser; - - /** - * Extra field value filters. - */ - fieldValues?: Record; -}; - -/** - * Prisma contract for CRUD operations. - */ -export type CrudContract = Record; - -/** - * Prisma contract for database client. - */ -export type DbClientContract = CrudContract & { - $transaction: (action: (tx: CrudContract) => Promise, options?: unknown) => Promise; -}; - -/** - * Transaction isolation levels: https://www.prisma.io/docs/orm/prisma-client/queries/transactions#transaction-isolation-level - */ -export type TransactionIsolationLevel = - | 'ReadUncommitted' - | 'ReadCommitted' - | 'RepeatableRead' - | 'Snapshot' - | 'Serializable'; - -/** - * Options for enhancing a PrismaClient. - */ -export type EnhancementOptions = { - /** - * The kinds of enhancements to apply. By default all enhancements are applied. - */ - kinds?: EnhancementKind[]; - - /** - * Whether to log Prisma query - */ - logPrismaQuery?: boolean; - - /** - * Hook for transforming errors before they are thrown to the caller. - */ - errorTransformer?: ErrorTransformer; - - /** - * The `maxWait` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. - */ - transactionMaxWait?: number; - - /** - * The `timeout` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. - */ - transactionTimeout?: number; - - /** - * The `isolationLevel` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. - */ - transactionIsolationLevel?: TransactionIsolationLevel; - - /** - * The encryption options for using the `encrypted` enhancement. - */ - encryption?: SimpleEncryption | CustomEncryption; - - /** - * Options for data validation. - */ - validation?: ValidationOptions; -}; - -/** - * Context for creating enhanced `PrismaClient` - */ -export type EnhancementContext = { - user?: User; -}; - -/** - * Kinds of enhancements to `PrismaClient` - */ -export type EnhancementKind = 'password' | 'omit' | 'policy' | 'validation' | 'delegate' | 'encryption'; - -/** - * Function for transforming errors. - */ -export type ErrorTransformer = (error: unknown) => unknown; - -/** - * Zod schemas for validation - */ -export type ZodSchemas = { - /** - * Zod schema for each model - */ - models: Record; - - /** - * Zod schema for Prisma input types for each model - */ - input?: Record>; -}; - -/** - * Simple encryption settings for processing fields marked with `@encrypted`. - */ -export type SimpleEncryption = { - /** - * The encryption key. - */ - encryptionKey: Uint8Array; - - /** - * Optional list of all decryption keys that were previously used to encrypt the data - * , for supporting key rotation. The `encryptionKey` field value is automatically - * included for decryption. - * - * When the encrypted data is persisted, a metadata object containing the digest of the - * encryption key is stored alongside the data. This digest is used to quickly determine - * the correct decryption key to use when reading the data. - */ - decryptionKeys?: Uint8Array[]; -}; - -/** - * Custom encryption settings for processing fields marked with `@encrypted`. - */ -export type CustomEncryption = { - /** - * Encryption function. - */ - encrypt: (model: string, field: FieldInfo, plain: string) => Promise; - - /** - * Decryption function - */ - decrypt: (model: string, field: FieldInfo, cipher: string) => Promise; -}; - -/** - * Options for data validation. - */ -export type ValidationOptions = { - /** - * Whether to validate "update" operations based only on the input data. By default, ZenStack - * validates the entity after a update operation completes (inside a transaction), and rejects - * the operation if validation fails. This implies the entire entity needs to satisfy the - * validation rules, even for fields that are not part of the update input data. - * - * You can use this option to toggle the behavior to only validate the input data. - * - * Default is `false`. - */ - inputOnlyValidationForUpdate?: boolean; -}; diff --git a/packages/runtime/src/validation.ts b/packages/runtime/src/validation.ts deleted file mode 100644 index a063cdeb9..000000000 --- a/packages/runtime/src/validation.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { z } from 'zod'; -import { getZodErrorMessage } from './local-helpers'; - -/** - * Error indicating violations of field-level constraints - */ -export class ValidationError { - constructor(public readonly message: string) {} -} - -/** - * Validate the given data with the given zod schema (for field-level constraints) - */ -export function validate(validator: z.ZodType, data: unknown) { - try { - validator.parse(data); - } catch (err) { - throw new ValidationError(getZodErrorMessage(err as z.ZodError)); - } -} - -/** - * Check if the given object has all the given fields, not null or undefined - * @param obj - * @param fields - * @returns - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function hasAllFields(obj: any, fields: string[]) { - if (typeof obj !== 'object' || !obj) { - return false; - } - return fields.every((f) => obj[f] !== undefined && obj[f] !== null); -} - -/** - * Check if the given objects have equal values for the given fields. Returns - * false if either object is nullish or is not an object. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function allFieldsEqual(obj1: any, obj2: any, fields: string[]) { - if (!obj1 || !obj2 || typeof obj1 !== 'object' || typeof obj2 !== 'object') { - return false; - } - return fields.every((f) => obj1[f] === obj2[f]); -} diff --git a/packages/runtime/src/version.ts b/packages/runtime/src/version.ts deleted file mode 100644 index b8e941547..000000000 --- a/packages/runtime/src/version.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as pkgJson from './package.json'; - -/** - * Gets this package's version. - * @returns - */ -export function getVersion() { - return pkgJson.version; -} diff --git a/packages/runtime/src/zod-utils.ts b/packages/runtime/src/zod-utils.ts deleted file mode 100644 index 48d3eeace..000000000 --- a/packages/runtime/src/zod-utils.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { z as Z } from 'zod'; - -/** - * A smarter version of `z.union` that decide which candidate to use based on how few unrecognized keys it has. - * - * The helper is used to deal with ambiguity in union generated for Prisma inputs when the zod schemas are configured - * to run in "strip" object parsing mode. Since "strip" automatically drops unrecognized keys, it may result in - * accidentally matching a less-ideal schema candidate. - * - * The helper uses a custom schema to find the candidate that results in the fewest unrecognized keys when parsing the data. - * - * The function uses `any` for parameter and return type to be compatible with various zod versions. - */ -export function smartUnion(z: any, candidates: any[]): any { - // strip `z.lazy` - const processedCandidates: Z.ZodSchema[] = candidates.map((candidate) => unwrapLazy(z, candidate)); - - if (processedCandidates.some((c) => !(c instanceof z.ZodObject || c instanceof z.ZodArray))) { - // fall back to plain union if not all candidates are objects or arrays - return z.union(candidates as any); - } - - let resultData: any; - - return z - .custom((data: any) => { - if (Array.isArray(data)) { - const { data: result, success } = smartArrayUnion( - z, - processedCandidates.filter((c) => c instanceof z.ZodArray) as Array< - Z.ZodArray> - >, - data - ); - if (success) { - resultData = result; - } - return success; - } else { - const { data: result, success } = smartObjectUnion( - z, - processedCandidates.filter((c) => c instanceof z.ZodObject) as Z.ZodObject[], - data - ); - if (success) { - resultData = result; - } - return success; - } - }) - .transform(() => { - // return the parsed data - return resultData; - }); -} - -function smartArrayUnion(z: typeof Z, candidates: Array>>, data: any) { - if (candidates.length === 0) { - return { data: undefined, success: false }; - } - - if (!Array.isArray(data)) { - return { data: undefined, success: false }; - } - - if (data.length === 0) { - return { data, success: true }; - } - - // use the first element to identify the candidate schema to use - const item = data[0]; - const itemSchema = identifyCandidate( - z, - candidates.map((candidate) => candidate.element), - item - ); - - // find the matching schema and re-parse the data - const schema = candidates.find((candidate) => candidate.element === itemSchema); - return schema!.safeParse(data); -} - -function smartObjectUnion(z: typeof Z, candidates: Z.ZodObject[], data: any) { - if (candidates.length === 0) { - return { data: undefined, success: false }; - } - const schema = identifyCandidate(z, candidates, data); - return schema.safeParse(data); -} - -function identifyCandidate( - z: typeof Z, - candidates: Array | Z.ZodLazy>>, - data: any -) { - const strictResults = candidates.map((candidate) => { - // make sure to strip `z.lazy` before parsing - const unwrapped = unwrapLazy(z, candidate); - return { - schema: candidate, - // force object schema to run in strict mode to capture unrecognized keys - result: unwrapped.strict().safeParse(data), - }; - }); - - // find the schema with the fewest unrecognized keys - const { schema } = strictResults.sort((a, b) => { - const aCount = countUnrecognizedKeys(a.result.error?.issues ?? []); - const bCount = countUnrecognizedKeys(b.result.error?.issues ?? []); - return aCount - bCount; - })[0]; - return schema; -} - -function countUnrecognizedKeys(issues: Z.ZodIssue[]) { - return issues - .filter((issue) => issue.code === 'unrecognized_keys') - .map((issue) => issue.keys.length) - .reduce((a, b) => a + b, 0); -} - -function unwrapLazy(z: typeof Z, schema: T | Z.ZodLazy): T { - if (!(schema instanceof z.ZodLazy)) { - return schema; - } - if ('unwrap' in schema && typeof schema.unwrap === 'function') { - return schema.unwrap(); - } else if ('schema' in schema) { - return schema.schema as T; - } else { - throw new Error('Unable to determine how to unwrap a lazy schema with this zod version.'); - } -} diff --git a/packages/runtime/tests/policy/reduction.test.ts b/packages/runtime/tests/policy/reduction.test.ts deleted file mode 100644 index 95a2e6f2f..000000000 --- a/packages/runtime/tests/policy/reduction.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { PolicyUtil } from '../../src/enhancements/policy/policy-utils'; - -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('Prisma query reduction tests', () => { - function reduce(query: any) { - const util = new PolicyUtil({} as any, {} as any); - return util['reduce'](query); - } - - const TRUE = { AND: [] }; - const FALSE = { OR: [] }; - - it('should keep regular queries unchanged', () => { - expect(reduce(null)).toEqual(null); - expect(reduce({ x: 1, y: 'hello' })).toEqual({ x: 1, y: 'hello' }); - const d = new Date(); - expect(reduce({ x: d })).toEqual({ x: d }); - }); - - it('should keep regular logical queries unchanged', () => { - expect(reduce({ AND: [{ x: 1 }, { y: 'hello' }] })).toEqual({ AND: [{ x: 1 }, { y: 'hello' }] }); - expect(reduce({ OR: [{ x: 1 }, { y: 'hello' }] })).toEqual({ OR: [{ x: 1 }, { y: 'hello' }] }); - expect(reduce({ NOT: [{ x: 1 }, { y: 'hello' }] })).toEqual({ NOT: [{ x: 1 }, { y: 'hello' }] }); - expect(reduce({ AND: { x: 1 }, OR: { y: 'hello' }, NOT: { z: 2 } })).toEqual({ - AND: { x: 1 }, - OR: { y: 'hello' }, - NOT: { z: 2 }, - }); - expect(reduce({ AND: { x: 1, OR: { y: 'hello', NOT: [{ z: 2 }] } } })).toEqual({ - AND: { x: 1, OR: { y: 'hello', NOT: [{ z: 2 }] } }, - }); - }); - - it('should handle constant true false', () => { - expect(reduce(undefined)).toEqual(TRUE); - expect(reduce({})).toEqual(TRUE); - expect(reduce(TRUE)).toEqual(TRUE); - expect(reduce(FALSE)).toEqual(FALSE); - }); - - it('should reduce simple true false', () => { - expect(reduce({ AND: TRUE })).toEqual(TRUE); - expect(reduce({ AND: FALSE })).toEqual(FALSE); - expect(reduce({ OR: TRUE })).toEqual(TRUE); - expect(reduce({ OR: FALSE })).toEqual(FALSE); - expect(reduce({ NOT: TRUE })).toEqual(FALSE); - expect(reduce({ NOT: FALSE })).toEqual(TRUE); - }); - - it('should reduce AND queries', () => { - expect(reduce({ AND: [{ x: 1 }, TRUE, { y: 2 }] })).toEqual({ AND: [{ x: 1 }, { y: 2 }] }); - expect(reduce({ AND: [{ x: 1 }, FALSE, { y: 2 }] })).toEqual(FALSE); - expect(reduce({ AND: [{ x: 1 }, TRUE, FALSE, { y: 2 }] })).toEqual(FALSE); - }); - - it('should reduce OR queries', () => { - expect(reduce({ OR: [{ x: 1 }, TRUE, { y: 2 }] })).toEqual(TRUE); - expect(reduce({ OR: [{ x: 1 }, FALSE, { y: 2 }] })).toEqual({ OR: [{ x: 1 }, { y: 2 }] }); - expect(reduce({ OR: [{ x: 1 }, TRUE, FALSE, { y: 2 }] })).toEqual(TRUE); - }); - - it('should reduce NOT queries', () => { - expect(reduce({ NOT: { AND: [FALSE, { x: 1 }] } })).toEqual(TRUE); - expect(reduce({ NOT: { OR: [TRUE, { x: 1 }] } })).toEqual(FALSE); - }); -}); diff --git a/packages/runtime/tests/zod/smart-union.test.ts b/packages/runtime/tests/zod/smart-union.test.ts deleted file mode 100644 index a356f673a..000000000 --- a/packages/runtime/tests/zod/smart-union.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { z } from 'zod'; -import { smartUnion } from '../../src/zod-utils'; - -describe('Zod smart union', () => { - it('should work with scalar union', () => { - const schema = smartUnion(z, [z.string(), z.number()]); - expect(schema.safeParse('test')).toMatchObject({ success: true, data: 'test' }); - expect(schema.safeParse(1)).toMatchObject({ success: true, data: 1 }); - expect(schema.safeParse(true)).toMatchObject({ success: false }); - }); - - it('should work with non-ambiguous object union', () => { - const schema = smartUnion(z, [z.object({ a: z.string() }), z.object({ b: z.number() }).strict()]); - expect(schema.safeParse({ a: 'test' })).toMatchObject({ success: true, data: { a: 'test' } }); - expect(schema.safeParse({ b: 1 })).toMatchObject({ success: true, data: { b: 1 } }); - expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true }); - expect(schema.safeParse({ b: 1, c: 'test' })).toMatchObject({ success: false }); - expect(schema.safeParse({ c: 'test' })).toMatchObject({ success: false }); - }); - - it('should work with ambiguous object union', () => { - const schema = smartUnion(z, [ - z.object({ a: z.string(), b: z.number() }), - z.object({ a: z.string(), c: z.boolean() }), - ]); - expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true, data: { a: 'test', b: 1 } }); - expect(schema.safeParse({ a: 'test', c: true })).toMatchObject({ success: true, data: { a: 'test', c: true } }); - expect(schema.safeParse({ a: 'test', b: 1, z: 'z' })).toMatchObject({ - success: true, - data: { a: 'test', b: 1 }, - }); - expect(schema.safeParse({ a: 'test', c: true, z: 'z' })).toMatchObject({ - success: true, - data: { a: 'test', c: true }, - }); - expect(schema.safeParse({ c: 'test' })).toMatchObject({ success: false }); - }); - - it('should work with non-ambiguous array union', () => { - const schema = smartUnion(z, [ - z.object({ a: z.string() }).array(), - z.object({ b: z.number() }).strict().array(), - ]); - - expect(schema.safeParse([{ a: 'test' }])).toMatchObject({ success: true, data: [{ a: 'test' }] }); - expect(schema.safeParse([{ a: 'test' }, { a: 'test1' }])).toMatchObject({ - success: true, - data: [{ a: 'test' }, { a: 'test1' }], - }); - - expect(schema.safeParse([{ b: 1 }])).toMatchObject({ success: true, data: [{ b: 1 }] }); - expect(schema.safeParse([{ a: 'test', b: 1 }])).toMatchObject({ success: true }); - expect(schema.safeParse([{ b: 1, c: 'test' }])).toMatchObject({ success: false }); - expect(schema.safeParse([{ c: 'test' }])).toMatchObject({ success: false }); - - // all items must match the same candidate - expect(schema.safeParse([{ a: 'test' }, { b: 1 }])).toMatchObject({ success: false }); - }); - - it('should work with ambiguous array union', () => { - const schema = smartUnion(z, [ - z.object({ a: z.string(), b: z.number() }).array(), - z.object({ a: z.string(), c: z.boolean() }).array(), - ]); - - expect(schema.safeParse([{ a: 'test', b: 1 }])).toMatchObject({ success: true, data: [{ a: 'test', b: 1 }] }); - expect(schema.safeParse([{ a: 'test', c: true }])).toMatchObject({ - success: true, - data: [{ a: 'test', c: true }], - }); - expect(schema.safeParse([{ a: 'test', b: 1, z: 'z' }])).toMatchObject({ - success: true, - data: [{ a: 'test', b: 1 }], - }); - expect(schema.safeParse([{ a: 'test', c: true, z: 'z' }])).toMatchObject({ - success: true, - data: [{ a: 'test', c: true }], - }); - expect(schema.safeParse([{ c: 'test' }])).toMatchObject({ success: false }); - - // all items must match the same candidate - expect(schema.safeParse([{ a: 'test' }, { c: true }])).toMatchObject({ success: false }); - }); - - it('should work with lazy schemas', () => { - const schema = smartUnion(z, [ - z.lazy(() => z.object({ a: z.string(), b: z.number() })), - z.lazy(() => z.object({ a: z.string(), c: z.boolean() })), - ]); - expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: true, data: { a: 'test', b: 1 } }); - expect(schema.safeParse({ a: 'test', c: true })).toMatchObject({ success: true, data: { a: 'test', c: true } }); - expect(schema.safeParse({ a: 'test', b: 1, z: 'z' })).toMatchObject({ - success: true, - data: { a: 'test', b: 1 }, - }); - }); - - it('should work with mixed object and array unions', () => { - const schema = smartUnion(z, [ - z.object({ a: z.string() }).strict(), - z.object({ b: z.number() }).strict().array(), - ]); - - expect(schema.safeParse({ a: 'test' })).toMatchObject({ success: true, data: { a: 'test' } }); - expect(schema.safeParse([{ b: 1 }])).toMatchObject({ success: true, data: [{ b: 1 }] }); - expect(schema.safeParse({ a: 'test', b: 1 })).toMatchObject({ success: false }); - expect(schema.safeParse([{ a: 'test' }])).toMatchObject({ success: false }); - }); -}); diff --git a/packages/runtime/tsconfig.json b/packages/runtime/tsconfig.json deleted file mode 100644 index 28708fe42..000000000 --- a/packages/runtime/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "lib": ["ESNext", "DOM"], - "outDir": "dist" - }, - "include": ["src/**/*.ts"], - "exclude": ["src/browser", "src/cross"] -} diff --git a/packages/runtime/tsup-browser.config.ts b/packages/runtime/tsup-browser.config.ts deleted file mode 100644 index 9689884ca..000000000 --- a/packages/runtime/tsup-browser.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/browser/index.ts'], - outDir: 'dist/browser', - splitting: false, - sourcemap: true, - clean: true, - dts: true, - format: ['cjs', 'esm'], -}); diff --git a/packages/runtime/tsup-cross.config.ts b/packages/runtime/tsup-cross.config.ts deleted file mode 100644 index 2ab45a71b..000000000 --- a/packages/runtime/tsup-cross.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/cross/index.ts'], - outDir: 'dist/cross', - splitting: false, - sourcemap: true, - clean: true, - dts: true, - format: ['cjs', 'esm'], -}); diff --git a/packages/schema/.gitignore b/packages/schema/.gitignore deleted file mode 100644 index 6bb80bf51..000000000 --- a/packages/schema/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/out/ -/tests/coverage/ -/bundle -*.vsix -/bin/post-install.js -.env diff --git a/packages/schema/.vscodeignore b/packages/schema/.vscodeignore deleted file mode 100644 index 9551337ea..000000000 --- a/packages/schema/.vscodeignore +++ /dev/null @@ -1,13 +0,0 @@ -.env -.vscode/** -.vscode-test/** -.gitignore -src -tests -node_modules -bin -build -jest.config.ts -tsconfig.json -dist -README-global.md diff --git a/packages/schema/LICENSE b/packages/schema/LICENSE deleted file mode 120000 index 30cff7403..000000000 --- a/packages/schema/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/schema/README-global.md b/packages/schema/README-global.md deleted file mode 120000 index fe8400541..000000000 --- a/packages/schema/README-global.md +++ /dev/null @@ -1 +0,0 @@ -../../README.md \ No newline at end of file diff --git a/packages/schema/README.md b/packages/schema/README.md deleted file mode 100644 index cd07891b4..000000000 --- a/packages/schema/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# ZenStack VS Code Extension - -> This extension only supports ZenStack v2. For the latest ZenStack v3, please use the [ZenStack V3 Language Tools](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack-v3) extension instead. - -[ZenStack](https://zenstack.dev) is a toolkit that simplifies the development of a web app's backend. It enhances [Prisma ORM](https://prisma.io) with flexible Authorization and auto-generated, type-safe APIs/hooks, simplifying full-stack development. - -This VS Code extension provides code editing helpers for authoring ZenStack's schema files (.zmodel files). - -## Features - -- Syntax highlighting of `*.zmodel` files - - - In case the schema file is not recognized automatically, add the following to your settings.json file: - - ```json - "files.associations": { - "*.zmodel": "zmodel" - }, - ``` - -- Auto formatting - - - 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: - ```json - "[zmodel]": { - "editor.defaultFormatter": "zenstack.zenstack" - }, - ``` - -- 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) - -## Community - -Join our [discord server](https://discord.gg/Ykhr738dUe) for chat and updates! - -## License - -[MIT](https://github.com/zenstackhq/zenstack/blob/main/LICENSE) diff --git a/packages/schema/asset/logo-256-bg.png b/packages/schema/asset/logo-256-bg.png deleted file mode 100644 index 8ef1fcf0958e22cb9012bd709721ac1e72db5c9d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18824 zcmcGWV|!#>7p*I{*|F`8ZCf4Nwo$R!LB~la=_DQ7w#|-ho2Q=loWF2BGz+H0%! zT62tX&qOIJN+H4H!2-T=Td3`dVzD2)^-H|5HSAx0Rv=a zNIufnDT438x<64HLIvLQygx zLL#6dwCh?|#j8FznAlp|&ST+8am5q%oHa51IwKXlVn7E0l z>47N*hFa^?(oi^oHm2QkXJaFW_MNXpuLK-0ya&9){|MQBvm*nUSWwhh>)P1(w)Obf zs&%N$%bBjf-KAJIb)>(LthKG>de@T@$CA3PxjB8f#q)|NEHrecQkT>%)$=Zk%Tbjg zMn=?+pOT6a{T={LD)jTKf0xU+0Eqf4BQvQor;?wEXlrdOlL#|_Bp7Im359^cL8uit z65Q>drCFgTW4lyY*wP|pGGuzQc_4wP*6CI@XJ%?EhAa`2jC3_}(-CR>*EC&8ze{V9 zf|=}#xGP?taG8JqDG4p<{gZT1AiA)q#K7ifilfXOLK4BZiOC7GUOsI1?ZRMztS{On zARGf!*TnjIte(F9=<;wxM@Pp#l4eM2>zWcG5+%TA`#P!cet3b_?M`xRieTc<-^;6r zp+VFmY$S6__u3>nCZ_O>0oI~}#CYrun<3A4z|lE@Xw+mmNr<@+E&!?)(3jzb7p*_O+}lFOVtXW67Vw6_3?Ic_7z`PE)vwiH(cjUEP0c# zYrfOhFmxFe7N77_b<#Ea?gkoh$2^%F{0oIlIo2OzNi)rb{baG8`r%|lmmnVAz5tRW zp=aLb{+RXk@E=@*`>+S=c-%`_ICqc8YJ&{M>*+Z2NLB(8{6`@q2~N;FGHGk&zoyyd zEq`ju7s$|E_G?5T!K;!N{ehD7(-mJoe2Nl-Y^m)Xh1Kcp(_L}Lu1{$9ll8Fdoa_|$ zeCiayK6vEtR)>4clHo zglNDSPT3`r(J6O6r&f_p3^>Yzj?*Xo7bvI?&FEhXgp4&C`GL z%Gh8x2oHatW$tBZB;-)CAg??x@nqWEQ7`EDO*rmtwSFe>Vl~FDHT-z#n1--?SSss#viYnZl zDk?z$&F$b+qYcX?=5n)BD#3*W_F12Rkg!Z6=5V1)pO0=wC)j4;I<@;p(i`Y{fqP!A z7myFf&n_|QT0g8cV!B=Ebn!mca2pZrA;l2I=-{9)NFXQA+x_CX5jZB2dcyTWNfJ&@ zBt#G!guhhp4$vP}>j~N!OD%WX8$uGKq8O_+UcS_4KtQ1W1)a1Z%4n> zM8dOHHmzDqW{Go2Jru`I&(AN~9UYBHD!c-`_U^A{TqdWzXb!JOQCmR3m{Ot~4VV}5 zit)#EZn0tDSB_`H1}5l)ZWPN$a3jGtx3h#f3-n3(WDKiR%jyU)IPEtXhW}Iurfc2j zV$rK&AX6d}vPDXWgrU1eqXQ9z@h-OJzy1*Nd5l)&<>b%{^?cjxTNnNNZs#>Dagdvp zg`Y~NAv%H(KDAJbom*D#*hqRe{mT37wHf;lTwTXIdO&+vkr#%uxm>+W zlrfX5%(`BmzY}zLc=-IF9L~w(1)m>s!XKK)HS%IyaHpe7LXX`rL2UNO z5ajn9tweA0gJVD3=Zxoi~C3Q2gDV_v}4gd+~LgG^8)@b)-cJf6N%fGy)(rV*In z<-Etp7cZl>KwK2SFe-c!8M-9Q?J`hf%x-!pk7i$1!ooq?%;vDrtwfB+Z-q)6GL}f@ z8fyTU(cA0Z@)HaEd?^X7ybq(jx<43eorUqcKmJ$JOz}b>6cEgqJlbG8?Ev5Es#`TL zs#A+COlh)Qr9W7=IwqKzMM_EwK4KG2Mf)@0HK{*BKbL!#CU?*Z3Sc>dL9H}*Wau~A zernK_gia$Av>FFZB<*9Z+)eaxbkSV==d+<(Xfo%C`C_FjD-%(i`CSXjB@eEQ3jVk-7Ikwzf9Svh|CC)_?)s!GCNA|&5j@fh2YW$kxrZb^;3h@ zW^kMP1(|}<()BFk_86A)_Ac|;`VA%C$EWZ9Z~(Z(D{e7>B*gwRIPCtY!P+gF(ElZC zLhQVNS)psuztp*3a-qwgPljf##+aN@yGUgmy>ulY&Ip5z`yJ8Sc(zQ##zxn_#nBBE%4e72Z*w~p-`w0xPV#!4u;z6g z%BwQ$p8CYF%)I?qt=_I_@8a%GI`|Ve2SAeGzjVHOKfgcf60%QCDVIWmqJ)CS>x8ON zrJBK1TX=Kz?fY?0Z-}aL$~Pyk#cIPmMkuK^G=hm8X5GJIv%t#@Ln@T`g#IYY;b345 zZl7bqF0qVMzBF<(sUvY}wAOm^Ufu{cisOrwU~n)&1ewHWP$illvmK;f$QB#|Vw`Mr zoqO=Ywv-(f==-7Pp(`#!bTb{wy}nC`t2o-AAbX4PjuaV zCx51(DV2VU*N$_4LP<9a6yNm>-wiT0Dr(PgSRaAZ(>{I+-eUC+B_bGIW}RqlEiD(! z?Iv#U@pO9Tk9Vhz5gSsZ3WyYT7$~UG=}`0F2;r7Ww+uE0CJ*&Us6Xl_b93m3mHo+D z;v_}3%hlw8U#@OE+vpJ_%;Q&K!@f;dTAM-LI8)u9AFs1nuaz>0T>Hfb(@&4E8*+TFKZ{Dslc?72Op#tOAGQn!Lv`0uIv*w+HW^ zW;NMI0_Jra0|WI~Pg9EVh3iU1luMy}bjs|}bWcvR`Cj!1jPuLR_BmGE7&M-{6)4ws zA7pHZzeLkF&f7ZI?mPir-rjN+hGu4=`*`_ev;F;I0fL`hoo`R`qd!YdIAi8&_lA$B zF52IlkMhiwgYyys{NLvCgCHnAaA$*&vN+XXh4P*SN)%o~P8TcX;K|VdDJP1b4_ujj zF~pj}1|b{m7L#}j$qIAfNcht5H(@b2*2q`yrSx8hJ_hOsV`;?}cE7P*8K|giep1C1 z7dpx;qc2ogW3YZ1r|reYfakMy%=7T^oE7VF(l(mEESCQ;dy*m+^YrvYi-}2RNj z;qdc6aNHPccQjJYhh3b~Z!dBT)`Ip%%uUy<)KOiY$|*zWwqI$9N@Fyfy5dqc$DVZ@ z`bb}Ew70z<1}j7blJ(;~hpf+&xU6a7KQ4nsZfSD9XY%>Fko$i(t1lceTkHaDC z_XZ7<{4uycd%m2`SDf4YD22t1pK=9u+OB&AG;K*Dsj{3RQD;8n(nB98L%&q7x11Fft zet>$Sw53$^fX_LTBJ`C~KwsT?3m=Co7@Jd4WoCyd4b~Iv>%J{4cvmUCHJ%%Txp%$t zH}3#+h^JpQU$%k{2!-JtL={_#7r64yH4qao2WK~X zR>U!n^dE2YmZn3>H@kzQzgXUq3&0`o?aq(#=3NZlSy6788gY*NiHV6bg*=f7g`9)C zo=@p-cTx49E;p=3=gbKL9|PCnJBsQN{J#hA`#*pBvb&$MkLs1{l_j48hu9UQc;RcI zLVJsH{qC)&q=HSQ5@w%--f2|8BSk>>x|AS-O(iMnyRSpYGK45Z45T+i93E$~=wI{k zwEB=|66uHU9pM7A(YlLrCeH3?ne7OaM_^vD)2SKq2AJW z{~Ap-`0Z#bQ10;?x{c@xsb+%b2iKODJ_B0O!ji^LiDeHX_fIkRz^^+L;zFM&Q)C`! z(34mEt=>Y`0F0r0zi7Q(^D*!M<4gOO|DbxgQaVkdyj(PJ&DIrny*Fz2^b{)s`ClFIr!mrbg4gvkof(o zmy|afqZY$B*l{_%U1n(m*emA6RVL@yee;LAF7f(~E$gMt-~(#5H9cLg)g|nY{D&HR z(&_TUPis->O`QfWIQz}Eu)aQW7e*W4a*4)x+HG4Jt(s19LmHDVmG$i1(PS~+qJJ5Q z>9rt$J7KBPaGGetIyp2MhlIF`E^ieEg@A&w+4E{MYc41p=egZ)-)gH$ATGc$1#=`n ztvj&0%VkO8j4tJYt`2%FV#hDOZnM+(X>M>ly;LXl?>XNIos6_6w+q11#DUwZ_DdfQ zF(tv7!fLsS@Z~KpmEFD}pjxB;4agk4B*r+pCUMvMX+K$Nkt7Vi#BaN$o|9ZRK&c1)J^ z>$l>NXwCNNJ@;EQ7w>?+?tqMix?N|m=Nw`K4TfZ3Pt-;oQxykFO*zII(-8V zp_QkhWU?X*N7MwM*1hbM5EpY97z9b^x4F^YK2$1RVy#>E_Cc|IJIZO^Og_X|$(ae) zova=gQl5k?J{no(tYeFl_!jo6c`{qc7w`hV#|>^rov75v=E<-!HNv zz1xrgWvFG4||lv{MKt1bT{dz(qp-^2m0GA7T&@kES^_J0dwKYML~4$_gh6?(Oz zecd4{U&KU;mM$WY=m*?RNlf*q+bt1&q(z8VoAtVMoUH7PBt}l)EEeZS?0E_|9rA`f z-t~@G2B5VRCiD3N#mv@J4ujf{J^;|TXB(qhEE^34v=IxvkaXsLB-MqMrW&KAaTW~R z3FhCNAq{zfHBypGW3n9o9!V~iS~QqdV5$u6^gM!Ab`T#Ko8JLB$ZvQvf4eadb-%p> z@%piBIM82>-OVWyiHyukfaDB`%e~F9*~%PK&r;F@=9eoLM4Kh*_$V+iqlyjYM(WU$ zMC=Q}OwNg`%Sp$Z*_Fd0^7r@_oWY*q+Qv?EDvwJ^M;A)3RTjd~SK;uyY_QtC237ag znUQ20zEZ0;dDCmp-R57LLRE*u@y{0Lc`A4$gj6#YGpZ`xZV@VyVQar=RSVo$DB!37 zJr5N=HnyTS*d@k9o8ic8*f?0a^>Jj<{bQEnTw-sCj4>|B0p3*ahVRqZyLf{}y9*`I zVZB9!=fGxL10ESUt$?uxT=}`t<$w+j>PMw&Dk4@0D)4MM3 z8*G=E@CK*4JhuX3B)h)^X%P_`$iyb0>z-%qitV$O=4RN*_=C}1e(}Z@kXt#iMdk#{ zM9H|F55UDEpQj;DhG#NWypRLv-U4#@WwQCb51vD$S}hYUF>!IFxBR~Z9!_UN1mCwI zWH?oEiD3UdoUVp|j7fPE;-R@Sa;ap#7*WOET`6kP1$d)f<@uqwqWSCKuo!kBC025F zN3aT$`oPb=usLID=ALh8`kv!yjB*$&C(EVA5VThkG;otVXcqq)w~x8Tsh7-V0s(7M zr>Yi?7#&@d2aIxbmwtCx3>Teon*{ziIMyn(puife$0_=XDuDUn-n+QQ+`C!WlDMI? zuCcN3Nvd<)VuOgSxh65&DQqXWh7pp0eA(zvKebaRxy&!46bzPRLj6#mn#JRN^=yyJ zO$9!WOSAIA5=0eN3U*(}p5U!P1hj_-8SfQVY@06caZT2*e+>>22sq4XPIDmJFsbjl z_i`)z17NFJUGPRAgZD7SHwWaPCSh-}*x-#q%(ogT*Ws}Ke4UQXFtf7K1Sn^O4Iz39 zj6m!a8evn_sU6J}^s^lthRj}#*T_rs=FiJ9tCVY2T1{p)vhAK%a&1b}ZjOnBqGCnj za=M-^G2%TvEYHE1EM>2{-7d(mQLxhjRz9ZdhXb!R{k<>sWE2VGkY*xXjuT1e+6`1{ ztQJa1kqCK1KVQ!rPv_gYI{QfteiG0C{(|p7{Nk`gZoT^mW_OO25exfE$^*$k3m7@f z6!45kbemvy!u<7qr(|QH2L@Hm%*)!%bt=UEwB^0)%WqKG#WH z5_yrZ9m-qzQiZ-$crX>z_IYc4gYQ+YpN6X2hxJAq2Q564)sud?CL`^IAV`{qBjV9i z{;Pwy1z$}pM`PvlfWacd|6VL#p6g%fI62QE8UZKx zT3m+u-6D45XVeDr5Ol+XG3gDm!|#%q+ZS`JAh>KF31o1-;w17_LSD~}8039X&ePKl z!-0p8+iQ(yyA=gR?`q0YFj6iIhtlwSfSjC6b_e;h<;o)~r5~ruCDX2aydvz07Vr0{ zIOn%tOxa3@X=x3aSK91RAI5!<*7Qvt7dj!*6!R$nl6m-xJX4~>4e`8t(CS^e4?z@QhzwMqEzg>@}k|1=GYv2W6uOZ-fuD3zXH4QsMY(uOCBp^;eWz74{VRPtXzCV82 z9;$M+kj(XTv0MsNW#_9|Dyl7mbVQqyJL}#&#CKS>Ofl4Ha5GZI!f#-a@$rAW?~aaM*tbeVyrffK@tGMjIm!8SLDk5+>{r0t2Nz%Vtq{34 z1}BeYqlAx&k{h;FjqQlwV2;o?*N4i3Z|gJFF+dxyAK zYe@A_h2JY~OF(ID|B=6q&sFOCbg3R&31sj@H!%dzNwXHfYXY0o)9+ITvir+_RLgz< zv1ele@^5dB@$U!vvr0ZBpSmR;jC4Pm7-(a&EIQr_Pcf}K-E9`BToLXNtHU^pN>Fxo zc0jd_tP8!G}EG^A{cVaV38dGF3*h77$?b(pC*tj8 zvA}jYm?l~*Qb!(TDHP!MIEdrp?ZRU6SBtZ=D>@h^J21!Kl;VTB>;2ueu2wd%fzQLo z>z&Wj=Op9)W@g5>;cHu@mSRShKujTqmwP7H|7|}qpM#wgaHqmw5TQ(<%D1SjB%zad z+wOM&p8*zCd-ZK2y_C|4eosljQ z;mi7agXjCZ^6^T9$-GG$M+ zoAp{lSqCwz9j>-+akos5BZuu0gUwRW$#-Ls2`ce05@8Aegd9sc2?M))g+XP$Cm46; zuv)-(aLcbbf^v5^b@QHR%U(At_E&Mm?z}w1@MU8IiMzweB5EU@#LpPWd{BmQgT`*B zoJSCijkN_BTDOJ*oy!pKS;%jRn8d=MdE2m7~@0kFD_YDyJ4EE1|27kBOt@DqX2`rV;`OztsY(!GxQOW(;WrD-*1VUxEULszEV0p2<##{B`hy zF~Ju3)bYfin0H*Y@o(z)u6Wb0IDC$Pq2GfeHP_d$FSr8Wc6Jrk^J$euP@qTZ;cT(E z⁢4k;}u6;ZsTIs#ljLl3wm`0hZ1iUXB_>DgW3WwYz*+S5r&>tmoNUb1p zcwAw83|FD8H^V;>7U$a;c2M~-vIar3hTV#<8K5D8O#yyjrMkVaEHQ{}P{ybuCu){f z1J^s#+)d368&r)DE0HXL=v_f-cvk4Yzk&4>El%a|su>uWo6B_i+^482g^}@*R4(Tc zdRA3csaI&-xK)ef0k`p&M|)>QJYE_|=vm0zt9k;vUu8w3=`}0K{6Xu1u>9uyyjVOq z?tE&NBJVFV)0LYNWiuQYDJiHV)Yh61=4-5UeZWX>SxEvC z#dj_*nH*6DwN-QTDq!4p_h~;p&k+Qmv3VA-;_>{tO=o|rF1@%Fgo`F=um}O%!C3YP zpK0I_uy{U|Kuaqjuirhax3`Axp{;I{eW7F=p6qlll|tqW$a9qWZwGV14xPs{r@OoI zu7@9T<3Fm35_Ox^x-%bTzP_sB8OqanJyOOh@d#|)jV@&#W zL?R<2y$*Rp5t*SNl`nJ}DN?!ZFNEm8wtV*7(SV>RPgT_q4}GvuuL2dht9BxV9`-IkM76cBJ3_Qr5aHE3*s7X z1P*C8ismr@497J!EVY=Z0@Cz=r^_(GCJVW3T;pVP9%;u@r+@-&u$i5c9g+TU!~!M) zQYW)3x&@p+0eX7bDH?sXsv}d1b&V5~9A8H?h`2?sLxTdp`-UU+(;uUotRhNntjRP4X z%(B1dbM1axv41mRMnV`B1?NT&sR2}qhY)MyLl3?zffrtNIXE7O!YESuk4s^Ot(Yh( zGGpDz;&jN~QKt>9a#j%St1s(9h0=$2`Y9QA+_TgFe?O|-Q*(L*8V z@!woPb535lVS(tMu0rTxGw#oA|8Y1h!b8?3F3A*kDNGu#8Es!Uq^zvkUBs)vsVPOb zyF-OJ91x-t%>b0JwJ@Qo2iNnu(XkW*0I&t>bS}=%X&Bg3?!!RikRgo33S(eIdtU>y z_4Vq^!AK}^%`e-2H~JbI7cu{Ld9?@D*VRRXjxP~nNchfITGckQ>V6&9YsCGxb&Qsd zjOliV~)u_BTr1wg|7RTM-9B}uS+z_@>nzNc+yx;9@Gg@PmrjlNEx+TZ^- zY+v>+1_n*W*LT**T)@Bpf8x`z`h~`1%>O;G7?Ky5llI61%O^;;T`IXX{8VehY zt^D720u2$Yv98GiEHX8r9^xHVM&^|Z;(sRrIVm_hu{X8B$2P>IOYxKHP=oHjy9*mW zn@>Z-!b0-~Zgd+61|(N8Tmo)FZTE)26e;7vYgJ$$9?ngWgDeJ#t%QPXU|7OuaP!qV zl|N`foWYWy!3-Dn^8-(1&`yrVqGxjbK(AY!nV(=VKj}@v_1pV%kGlZlg9DIP)1Uz{ zP$mBPu5OsNB<^j|L#gEpr2 zKkbJJDf;K=ij!aof@t6*Lsb47pGK#FhEO2jJZf4*1I+{u4vsWTRLatuQ~bdXd9ViX-dEPkd&*=Hn) zJ}@w_&G(AQ90u}>pH4F+Vb;9GZndt6xgiNA@sI$@awOyz%&xUW1&4Toni^(wNfc~C zzVzk?iqrk+W~ZIU-NC<3^KtB3QXlx=)WEKpP{6a-8~-ORHlU9lHbJjDAgAgkOSAyW z$#H0Cux|QPaHGXr%;(J#1>^SSmWnYyDxAcAt-;v!l!yn5nJGSX{QSM$^{uHvUv#w? zv{~X4F%CYpar;Q*2!X;w<)KDg1CWXVAQfLmB;a#@(_IY>Jy(lCU6yP~>EiC3N{XmM zldX))HKOf}`=-qH)YsQf+n%2kSssI64K}#|uGirtvbndO@P67(0Cf#QG8FR!6>yjg zX2f?w`&!_-oW-WTxWpV}*h$*8v@08Gim~E0W92T^Xh2m z6COKZqDctbL<49f!IOddx>x5$mL++KhCWuJ0L2?7!#Gc0aX`(yuC~r~RbZ0G#j4n1 zXXj`^DFhasy7UziCe{$b)SK3`=jCRxE|;S$k+8?j(#Q+3m5IdUn`bUVK#sP#y(xVG zdZ;!YryY$Hz>krELH-3C(+BLL8x?G&&7LoZ*Of{NxhMW&ElCq}<+J&GnJB?%Sc|*y z*b4uGV2n@8X;oM$h${VVD#i4ebY@vE<$@7MvK6pMa@tI9sv{$eVES^0PM;zyP)U`; z=V?*0r9T=Gj;j82Q}Ag2Z)Wr3?fAUaRlUC%I%n@GRe*k;F%`FH1_EbArZX#{h-YwO zB3rhlE&0ElyIriMf*cEaZIx@Vm4E-#%2bPK1uv5ro^?2E=Po;a2XE@WyZ`+wnv|Ch zd;A_2lcHv4e_>V=sW~&?sM?{$s?%-UzZY02m-ti7`S#%<^*auBz+Jze27(dFtTnSmd|!i5CPApn_ECxAtnH{Evzt?AoPS`jj~`<&^Na$*!8t zKeyWKcUix(bd^c@_;~lOl;AqrO(Q(3w-rHzq_nd#sIoczSAt`2l2kg4YrAtX-$>qrRU43hh)1L_LZqL5{#+G|$}iPXECur)e02K?nCLPAzqJTY&mfC4)jH ze+FF&vr<}G8vEh=bSWrdal6hxrKuRv-A>#bia%cewZB;6NL*{u7#hw1aP_#>D8`_U zm(wI6V5SF^VVMMQrE2X|7Xmegb1e9a)k=Z#S+W;vO~qGTS<>(l24Gi0s&gTZoB7$n zoZDT3g6JD_=$W!r&HLBVq`tm7wKk~@;I5w4(jcrdIZdVpJ|K@RY$$+Na^d+O9L7k! zDkZP?Ed?#6H`wW?(A)bRP8&9q-86(ZV}^bq3OglW1O}Ji2tc?JD4ZFxa*fgCxg~fy z(?zHOa%?&6)}@Wnb`~1tsMoq|sX^6cp(B-F$yx;cE#z1I(eeJt_Y4D=lFgoFgYoM# zX@|3|5@roI&t*Nvm_Lw!%`Y+uLYnY?KWe4iWYukucH<^EmOOZQPhdIU9gI2oce6*! z^q5p+;p5{oQ)@cxN-0^cTx33$QpViSs$R7(11j#OuFqCpIy=foP0c+soeTOgr?97~)s9PYp!@WskKDUirj;{cD= zgEBt0=kql4A)NE+et$>Ed|fvDW-#9%V@jv}J5&o?=U*v1HQ*P3rMdhx5cPoT|0sYs*kQqcN;S+J&N5I!!LWV&C(qE;k zO}6dF!^*4^1GRX^BtWcxl zvSRMU$T7|9%~4(%F`p)CiNs$El{c~m|2K~V1Z6V>0@7Mt)AY~J;XE4dXNx~uYGuD+ z8f}-;K8*}Te42C0qG%f(R-*@Zuvf9@21tde=kPMo0i}DDiMkb~5oI8j;`}BdA2bcB zWw4o>!N+ueX=|<S=1$I_g_so;6JQRvD5c*Se1V3D6Whp-R|cl+dj!SUcV z9AnpuNHiEfWVAYaBuvhKWN%3arBIZ_|JGJiLDGhObydd|1n_0R*Xm4Ve$Ue3aAe~7 z0@)7vX?C)`tk2W;fyY_Sdo-DzZ;ZOlDkSAs|0V7P{Zd6~faSlK#vrLZ**7>SAvxQ- z8r!fn!HHT_h(gR6%W1#HfGzB^ePgp&K{>2O?C2N;ZiNOqu_>d&KNfBo3W4;Gzck&d zg<+71ie4X1RZU#bCw~b<=rr1qCI#o8i8OevviY?7a^XF%l{b9#fcZR=NXDb#Zi7m? zA?qfBoqVyV%V)SPS%|fxAOXJ#DPonYTmh@;Pk+p8#piF9U(oJnwcTSOM1w-;a>C_= zl^id+91iP|9e=%R`y2|4#UkeZqX=Dk%<+1OU9*9woL16>pLfK;Nll=j==C7ouuPRw ziPveHM8j-&A_{~0KdZv9Tf|QZEyL|YBngjAIsJObb$0*` zPEN=bDet4Rqmv?3e`!*odOq;G*iUev9^jV(C|OM#ygJj>KLsQrtyDOFhi`F71^EzV zp)uudaJk)1q*js#|baI^HlZGkrWXT zR`Hk6+PXEN!t|QWauX5~-ZdVhPF?7E40Zv?g1fCXV)TEyXjX4krCfb+--hDx+lPg! z6N4$tN}^dv^oO#2_L(5mY7XeU$5hAVV)<~lKE!?Gd6>3l3%>O3skmBhMrpL257(?V zU`(^{X-Rc1WXj=H0cq++hYg*zbz4FO1b{(Nu7Hp725;j@-nZ|A+Vw^^CTq2>)NzF0 zB@dx2ASXWi&tZh;Pzk~a|9R#w+AdrCy^jt?Ser0z;NnEkO4NF9q0U`4!hRyTRbIOd zHmG+H*pkbd7oxk4|2n0w9%*!TuAQ4*L!EhVm?V>kR$gY(r^!}e&c18E8QuzbBK*(# z-Y^W{>EDO%kap!Zv$8N|G68xq+(K?^$y22=*~M<>3Lnw3XRLxACq{Fn06V*L*KQc8 z*`5A~B&^}jbVfr(mD$-7gQJPrpdYUfNgLhvSxsg%xC1R4px8t;#OJ~4U<{NNCH?v( zE~JntF9w5RG59cY{nl4VX1)6)G?Lx>%}!+D;O-l5N7ZD$i=2wntoOTfnh=Pk`FMOI zcB8yLb?qJ=8;fqcy{TooI#et9kQk09R(j%1f&2nxF5)@SwWuWORy4Sw0Igf?)41iW zd{uMVdGN=cCYxE&0RTow1oF3(9c=_;5pf8oPsh!UZ;bj~!5{D!E45x}$b^FO@Oxk} z#7_{S0oN#_CQ}2bZp?=9o47oxZL0RGO&tA*A`MknOO;t>uqIA%q{fewV$eWP6`y6O z(qK7l5WfsPT1s_(NWQ`^7I{4|iZQKj)+?p_TO1Bj7rTqOFUeLb#{zJ*Fd zXkhwn7fAg~_yL3X8^n5=XX2amN)02O69XZxb3CO+(bxoxDCIx)xE|kqBS)+__L#{L zB>{l&&?>D~RVwJ=S}vT70Ida6!51x6o+Ro-Y#PoXo;S^>)ars08iGissVP;NNEcIX z``jaWbxg#XZ5WhqvS*z3DXkdO^y9tD*b`Y&psGLr2Q*SLs;i{m%c|u<`wyiXMyuKt z>hFlMRf$m?dsipPBL%*?9d2dfuzuWsbpN=WP*)Ad5Pid9v=pRykZD3k<<)EOii#B3 z>3eK2?w905D3noQ5&1*I8Jr+QBxcKHmmm7t5yFU>e13RIP- z>JS7Lvb%-uZ< zGl0x$ox>bIWQNZ1TbFl?4Yg*Xa3o5c$@b1xgZFp;+Y_x!R^K$gPwyVTYPIsTbwla9 z93Edz1j*N<0D5{kyr->d9(Mg4#BU+I;Kx16sG(+}VppTx&P_2r6%X5|0_LxM$e=@2WHPO@x$(DhLf0S;cVbgA_CMc+!Eg+}gHx=bSokd(6dM@hH8 zvw!4$!z4nxS-Q3~&WFc9tE(Ug&|Ft5 zv`~H_wfFUAc>DNBB{oa~1;~%Z;P_JZ)~|kuImZ`5Xagf?2$fCE>$OG--23<{zkl0))W zj^U0+bN$sZL?L(QIudKZotVUzG-V97Zym9tex6f=y4I+b#Jimz|EI|W-r%EUm_^H2 z%xZ;4lneCT#JCclg1S(-$;fo>JE&^4daQ)sK1)>=q7{7nK()tblFY(M(2P-c*GCa^ zu{~HVI+7*;(C0aa{tjyNaCv}75Ux94Lj?={oM{2;7n_R)+;;6?2uEnzK5lNdc&mwn zrG|ll{TNwZPG4Uo7{~@AU^c@0U4hH>t1zVS7R?H^LvaOmX4R8b$}8R}xO`4xh~gyDZ-2!I|@H#a9#R_?*v59!S^ zFl=U@roih@U-h~o`ip;o+i++-NTGy~ngG@4{?+Kn*jP;G9wSwDToHUWZfqPTZ3a#7 z3Wx?;KG#D;1SRVbMQ~Q}>(`wJNjh!=u7-n8?>V!zCR6Ic!6AXKEbZ!@e=i`2sEEbx z;I9UIyyxe*pWeu|?z832S{@I7B;s7KWDqIB4nG-Qxq~7Dgo@X*ppD#rzBfEiz*j&C%ypdIP_KGrwKII#?TE z<$XKCy#%g(0&h=B?w^F_DPQl%{E6jiptx*@5Tn2q%pJd~yT3>a`eJ)_hkxe@xJm}` zK9v;rzyg5YOQ24dwg3UGS8y_-*eob93Gk4r*-*!!akn%OH8{5HwvG3U%4E<*are&+ ziR{`t6rozbood5Wr`b*~c zI+xjs6G#&mN8ZQChlt5sp$n3w&}k%Z`kfJ;uwiA$z!;@uR_oSB|J5uLg~Ix|56&%* zLYcP#ik8?EH>Q9!J0LM@T76Z*esOieZoE6-rWCWJs`JeR;Kx2J4*t{oUT|?3CE-f56n3Bp_9Hkgs8P0ie0;-te2Foc98lxCP@(

&a!tZdv~m+RNdu%!g7N4FN-6aaToN@|M7DltY z*zWmT&m9T91V5=hMAbgCGs~=mDaKNq3&XyqQcvfD6@z%eizF7hq)>SMo?-vvI2(-C z^#`Q(Ffadq3JQquU& zK#Qp&&n;i2L!ndgXKyVn9pwzw;^TA~q4Cm}9dg5x1G`#*>L=pF!Rak7*H<^~cZ)J8 zOQ;NRTd?Dv;ET1i2nAP+I{?`ylp#G5XiT~vLtyYGApzKy4 z^Q*o|!Pi;&HTYJa+Q8+1Q7ECAqUCR)lY?J`74uab(awca*s--vD z+5Hm)^UWIq89AOk)bpmONF3fIM)-$uDTE6!2vzGjz9Xa`G=rCpOo67y2!RC}-ry0E z9B!UT6^PmJ4;bXJj7GoKTQVZ41fri-5@fnmT7c&j)Ysu7Y*ej#7s%%~XFC_>1d6J9 zvRt9}dkntUw8*2c*0PS2x?>P7!=%7C)3cWv@Ifhe=g7H3I`d?h57kjwi3IAGN$7gE zV^}QLT|SPtku(N77M6B=%I##o875X;izxm)K#dIWe;w(Wwg1;V?raRQ7f=5DG`Y1c z9?M;*QqZ*l@ls~EebY5|gDx_dvMA@OQ&UU}eC7cR9bNKY%}P2n=yk7_ z0t7PEqW%K)XR(_n~bT75(kgA{P>wK6qhRL&-KzAGacH<9m$fk=G(v#At=?vxsLSiWS zt`651&8Gs*bHkoijiKKqw0z-0eTnsm#%QGWcyMacDWYuHuM8_LuvmFpE!N~QN8h5Z zwqTJ13KDa_2?>!$AngsirL#MpuW(ED`mEG@5?}Du2+a$A0fO~PJP7ev{b)e$kZio; zP_zmG*j#WJv?T%dZYq5ct-fJC_&YW(CWTsfts2s{{tj4X_%%|d0iP9jHH#5CoYcxo z1R~MIA2WTl@EP9%g0X0MPaM%pFt#<8YEv0>=KzO<&0t>H zG$%@-P5ppphv0=Y^5ypxHLNh}pu4{|i*)LF-7p+bBUJ0%UqJREZl8Crwpl1bR0_mo zV4?C@)+&ZTwkHoJ*Um_ASZEH$s^f}Aj@qEy)^_WRhK7mCjx@wgKXNP=h1|{-I(iOQ zLR!g`Gpw1j@b9G+|+Tsh|w{MY&MqI*7})8d3ysoNZXc|)dXG94@fJ4AU z$TeuvcNLSxSy>2AhSp=uqy-PlYGOH2jgOCjf{pa)@gC5&^?!Of&wsY}HjXEXqoS!A z(bgVERn@9voe&~Mh+S$+9kqJu)H?P|G>FEC1~qF(%_>^Qo~7x&MLd!S~Vkx<2ps2STC)=0*L5Km?>W(whbz`lP>i$QC8Ba~$k7mmhWzlFcL1 zN4$>hzwo@V(;rFkq^!aB0!T8Wp1&dZ3t~6-GU6;DG0+eqGfO#MYaC$`^=A7f+CH_< zdprhwu>I?|cU-5tywXNp76#^mFfa^j3FDrEV9n+MXJl2q`4{1(9lAh4H2U*+nXJZs zU(5jVhD59VbH`L(qTY)1Q>xzgKN&05&K|;UmbzE#uKHO^;!AI3flr>-%-c^h#hS+- zY;Daq=B$n8-560BFWb68WJ#)9o;l6@Fb8q@p)J=^+df-hmane=;{FROT!-%p`^HDy zvamDXlD@9Nr`ERty`PKr6!TFRb*@8&HBmhV$t4W}3h;W9-*(0BJo=ht-un>Y#`y9& zYEn8W984fXAz6FBw@g!=9Sw4rN~p^`evu!izUgN)IzNy3A)_9bx3qu0ei`A;#9%4W zP!c!CjM?dy8oJjCPUMLx?>s%M4qhk){C(|77a^BZCDCY9WcV@c&H+I*N-SC%S(sQs z&o{`9_w{eb@Bklv2OX$T5XnT5o362-V z2d}wbWAOakC`hN<^28=a^A$7>EO&s(5hl;%O^&R+f{!6G zxvlN&PM9fd)YE0rC`E@&R=|Yo67TE@d{T=GFwx38)2mAGU!?}Uq#P6kFTjY%-G~ej z{HFuZ30cq9OK=q+$X65yJ;H6AZ0Kl6gcSuS}8g(HInt_Uq6@~&;dt)zN?5{Y`3Zk5%~5^8)KS{h}(i~pI_45-_gq}^TZBj>n##rT6%FKID6^ggfY83#|Ri@0a&jy(ZCD?9)|_ z3UpmJx3y~zlMd=%y)HUyT~YI%vhYCBYix*oGyhKPn&jsTeA>p?4P5HhKilhImXgcO z_XP4WAL3I$ZH$)<#oVJ6^lHa?wS|nh8t2Sxe1JwS3?_1hchyY;|CpNf<7h+|_ows5 zKoK0Jm84+}U@CN`Fa@&zYbCb*SrvxJidYgOPl6n z3`rv7zg;sMm@03&($14?r`F`S-hB^V6&|a`cmq;f*Wodu$1vIO?szKIj09CV`{$Ty z*zCJ^Tr@kDHS52@3v_mHMX*T%Yiu`C&$Z&Bfo?Al@;W&=d#Mj>xr%CZuRq*7OymJ+ zfeGT>H~=*N%bnXT*w7SQMXhq?3T%|MClQ7PT=obN-b@40R$WQPsnYAici(+a!L z%&5rs0QMh5nHDn!sc^#JVFKshzjXwjFY6m)7j@l`MZ>B(=b#qoi*P>|h@Q{%qbX71 z%BwdxN&foga*5xY9333cLxI-k*Ow1~eAitBiDSQX*SIFOkhL?WvmT7Yj5l+)apG{e zK24pu+jtSbh=fpXNmKMhxrOFY31zCEctKeg#k!;qS;(7^xzJ-pK7kpf>sF%EM-%q$ z3pxsBMqta>%PnFqd3BU p`+%r6|A*$Ee`%Zlt$=8l2{OhOBEr|7bpypoAhf<2ss@RT`wyl^_tXFY diff --git a/packages/schema/asset/logo-dark-256.png b/packages/schema/asset/logo-dark-256.png deleted file mode 100644 index 599faab7230024241c3ab4fcec7e33ac47476d61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23301 zcmcG#gLfp|_dVRPZFg+joJ{OwVrydCwmGqF+nOX3+fF8)*l$0d@9$rDd#zs8y;jw& zTj$<7=j?sgeAn_LgdC@CV4KmCsLeq@6w#NgNA-l7G71pVse?Ah!3|wXNB- zooB5x3MAXhL*Kw2|*#@H5T;zci8Z7zS7pu zYJ}*hDms9QCT(m?W>BK?peQXjC9fze8~QfYzN)DrCxMWn;)~3Bx z$gRUTYRHn05Gt02djFyXjM&4J$q*w81rG$Q?K#cKU1FC`*c9T&O43FIkT9Y5?J&zJ zDwYkkJZ0JU!YO3)Bo-&o#i}u-qazPm@3s?cE1-&RrT`IuK{PZpz!M9Lv6I7N1+xaN z7q4X6wM(aw1sY7=oPqp7Qi|Y#fR>*v#C0n_(oz!YS($`-(T7F+U#IYVZEbD4HZDg+ z8Pr&nmFWr+$}l4->4f5`DHEjx;>RZ4x&#po`oBXI^al?BLoETOgeXem9uZ3TfnmY^Fl(!NuO~LJs=7aIAcOD zP1)8&i&QRd7;Xt2b!X3Sab2N@@M0xZf6kx9U9Gm&9W5vdKbS=5L!k!{8(wd3B|9}b z5u38&H7c=C^p6+gK9hzZhX-Fc@Ug^C#=by+#VC}S5F*0_BO~Q7Z90+?lQVJPBNeGs z^mPO8*9l(42R8r^WQQ>6L*Uv*Llf<(QL5RZ9>K(Q^oxtDPKDjo7g^pUJ~xnI5ta#n zO(62s2DczN`GGoDbOE9X1#XVCm`8>TS(xsr@@|>CiwpRCojAwy4pI6YNwjIij3_(? zSa3bc6|N|Sk6ZWxa^R@#m;$D6x9NmzEG}6g$kM(Ey&rB5Q=8df{;?`_Z$lAVWe~L? zJOT6=A?=c9nZGMfET*&CaK!Ij9zZoQOf z$Oi8(4o^KM`*q>F57XFH-#3;Yf8wIl%$`sGqE#)$Wl1sb-UEWQBUfuSh$ZCws&w+u zMKQH7XFR_3I3)Pl7+kTnCxp7PXFzx#r{Gh|jkY3DM!v25yz)n$aKTZ)kqFsH0<+*i z@XPBg14)7^Jdy{Zos>dsJOXg|@Tsd0Ys1CaMi`QP6itRL+hca3Fhm|Nus?{(4M_o5 z=}$NC!{ax;F;wpw`DdeIJgCc*zHzxjJr_tizvFo8A&`N<28msT)wE`lF>^8k{%K$W zccUN@$^y$~JTO~_6T$wx+ige_0p-&hcjwz{pdalaAO{O(iXNe$P@u%Z6n=2{&x0vZ z$?ykFO4@1$E=<;Rsc5|lJ}gu!2Yl)X5Io7DL&FH@0@p|^lss_LD8(eKqOQKg<+wi# zji?2+1&?|?j7Rq;MTNw^>PR_ zDSgeDfBe8{WqD~yE1q!6pwZ(N!3xoFOBCDz%csv=-skImQatjvrJkWuX5V>sUz)@${x*~t!R2w;- zf5Fu1MAKFyMzX{RJP-&=&ZjVC)UFNNut0EcySskD<5=ip@y+$&2?`2|M!|tdq~;lY zDo;4?acX}IhQ9D1ch6~cXq6!?o23XX{a80Y(Log!XT+|}1i4In%&6&6W*D6~d?-hx zUH-?L9(o#zDM~X_y7O15ys+Qf;3TunFzN~(E zQwK}g!dYm4|IhS=$T#0!Kei6Sa1XF$EKy4R8LJlcQ~A=QC*kK^`>G#xc7v#T;BfsU zq`tj2YsGj?=DpHo^K4oo6k2be0VGZQgkaHY^N0ieV%S-}EZP+|4PBQ7Ve68Qw~h01 zun&~d#q)@;$~Q0IqEb|8kf9FKrY!9gY+CRYf?x`V_W-hIce`P#P5eS}xAau;_Z;V2 z1Tp^zxuKSJ6@@bTi(BvbG(y4F>yIa458vzU9%4jCbs!i|{92Pmyx-oM1@_bAZ(^wu zWsFYr{?2XG9B4_!@FnBzi#sT{{_(Y9_A{yPahS45d>{Q8tE=~i#T0(Jatx!!tQE** zvfyVzVA;RJPla>DN)jUCBoFUq%)tJPUDbyV4od*4$*&cIY`_oSZzCR^`AP}DUU^02 z2JOl)lAz;~1ihbpo@R^U{o+V#aVY?O|8`1b`?6VW*VQuB$jIk41>q9of@!*1<;#4T zc{xA27RKTixb3zDzLh3SPZA^$WMw9bh9%}fmkPrmTkM&#l~h%1}K z?71bMIX7tfpg@}bJW=@tOc_naO7(Wb!Tvmn<=*BAA!U-y!wF~@3b*KcU$?$S`aP33 zW!_Yu8)Vr(d-hQCYx$|l+Dh2(^W~%mgF4RN=-+T8GZFz?N^QcVKQFR4YK9z@%|fX> zZq~G${4aq_3Uv6)laNVEj?hVkJioJ@ug@pyF1O1@;|PVbCh&TfGZkWqit9{H-}T9# z6%{gra=nQn;V97Jqy*UTIx&}}$zzjo{y-dur7=YfMmYr;KaMj0I2AHxeXGG|jg*_!V)?eSVp%7XPrTCd; z@-P9$S3=<yybt^C-EPUWD zESYf5nS&<(cJG0a<=bG&uk|~Z$ZNVxbsLwDq9HliDJjF_Q*z0r)YN4X-z(;M1QZZW zF*Sin`BB>nLuq~IsQCUa3@x323q6h(F?2lpHcFjQ=)k{divYID35a%=gBij)kQ zo(WLbxcy&_(&bK19gRbI;P`ssRqW8mdoXU1px@>3u@s5$e99 zXHH5*1LQkZ1$VgF9iSo3@=VvHc%g=Tvx|_I@)^*`LR!E>bj8m4*ug!1jtg?mGi%aR zq@<>p8^?ib$2dVn@Un69&X6f;K~=TMLH<$S)1!w#rWBPp)J3nTVS^A#b9lUb3^p(#+9Bb#<*=MOBq5{=H<5 zQa?QbyV0OfJ-Ss7wE_nN4b4rB4mZgO>y?7yq*T^7i~7b(t_@VKa3Ta+Dg0Ei4>Kq- zHxpoWU>dfoX5B(fWRgtY5^ij!Dg04#O!FeeAR~|K-{z0Qv*bEvuiXL^76}+%Nx+|6 z!ATQ#+-OV8_n*XhB=K8-h-^@DzfOGf29NM`S<_*A#hA(g%@{m%)FVZe$n?vE0-72( zw5s6AvYF49`0$n9#8|CWgk@2?-8)`>YuyRF+M}#gy@;601{bG0R2fTMeftCE$vzOR z7v5-3sxC=LVNavkaxYf;3Kfp*nu>n$qX( z7)i2ZJg%VU^9^b6_c+|&D`$v!h|uJU3}^=-xSKTnBWzIM19g|!lmD;`d^ycun}4=d zsLhE>kf9Bka^MG<`iRQOO$i~+aYN?!^u%4e&g+}A;l&46Ob>M88Fw(cY&L0B*^Tt-(I<0)!Zn4^1 zWUlSHWBY23ongo3CR$or6TegG1x?1{N&)~~QqD;{fF&*`Z7`>H}MGehX9Q;Npd|K*S7$|`sLoQU) z#Y^~=qf*#Oc_i`HK+y$|-0(+VX^=hX*$y ziwZoxfQvAguJu?bb}*m%*A^CvRWDifkdn0iK61{}?tVTqnH7xzp;^-EbX9$QyRG+w zV^#Klcd^~&Gz7ea%Ni?-6ep%;0!*qj3p0z$m$fx@U!tV(pN|f6U{YiWkv8Aj7?jAS z5T$CUnpQ9FPs_{w2)J!uVT$#2x`n=ORyCJsDg@{%{g(rpblU7WjeTRX=D|U@J73`T zWQ?mGcWN!gE_Y_ft|7$z>e*8$dgS5o%Mr%ukE#I z&yD+?qwOp_(Lu%SVeJ=pe_kt-es>1Tx&3P--AxxPba9E}`Nle=kI>gdaY%^rCDp?3 zWGt*5g-kZ6jlQ;)EO+ukz*!pwC z24K{PDdmWWh}l=*&`m#QRBN}D5}lK8s$q+Wiw6gc8T%s7 zeR{IoxJ_ETmGt(M_1r(-s<09H=LGGMjE#jFB35&YcS7%VUNCT z{>I<&bWmL?}orD3tcS&9osjH8oOjT3Bf~kDn#rR^|MA z7cKb7gqxQ;6v5$Ql@&X0#3N#QPe`_Gp=;!V+Ubocy{qKnaKeAxCe7h15WfrbBa;pWG&sWVKbLW4Rl~vq+4-Y?TaQBm%+uC5* z+1P)qPo)*9WZw6p()zg5V8%7B5lwRbU4{-!P*mg52*OkBV_F7uLl;A)k`=Io5)Q{* z(=xm>F_9|cmLyO@i3fgj-}Zg#J(^7CHsNON*#_5av%xb+OLDRAtG)Pe>tjTE=v7Py0(TFH)lfa6GK`X zIgw1=gu~crVQ%HLt{XfcF;DQ2rK@4Rbg#uuyzl7QbjI=PpO+TI@G%W%i$z-=kLT;s zYbD*rfuhi@0dhEhbIG@wB}?sVBJQ8_Xctr^KXd@jCx5Vt4^5{Ph#0r#*w*ZMd z?tFyPRL*bQ$8NHS;X`O5L#t%yaQ{?QRIWJiV}uF$Ts#bbn7<>FCZjF>WWEuFwVVI_ z`}duI%ceR&+3Ri*E;$GnyIlu}Ud8qGykNzbSsi9r0JATEw^HPTXCxmHQfP`CUBjkb z^UbCohV?GNi;Qrs_j2SgGMPFuWX2st)JToGp zzLkyjZM${Lw)KTg;`q9rEe|Co)lFn1NhMX)yScSj5PO=zJI%#lliT(oQC)ir+ssEWau*S%(!s()S`+E+h5oE$Q_a6TikAe)AOq zOzKW2O2CeLY|(C+_wv!iDbyIu09Y?_^7X#^St!$+!0df-J0{HUbU6N?8xq_efJDHh zDWuoieV@9bUy!*mZGw@UtnUAEhd~mXC$NSCA6`gWLesfS8cCiy8gK zf9LAokJu)jT=?x z*oe>{)V~#~9r(7g1}?tZB4&gOme00dUrzEAaWFCS6-Y=^FtOxq+O!Qy6B?3LIy^k( z)Hcrt-xi@nXH7NSHdUMKqRueMqnEU7#1N+O%^OCdRjewjl32eQWhS zZ#$;(e*WBAfBEq1t7~lh3mATHrBC_io+{(%>-qBxK|BnRHRk$&#Z-aG?+K46I;!0*_KQSWles2dhUIXyB|-{v^+15>O0j!ucsJ_ z3=jq+uAi{b>*1;l*tREXTgJ=@BUrS{dUJKI%m4Y7IyPYJnmT>5j#p8R z>Hto{^@#O=y8DN!Iu^u!5*`KtKbM7w-86`%=wqu+pQnd3T&c}d2}i{L2cJA3^_OX9 z=ipYe&2H<_1&G_t&c%0JGml|*ZlBX}P*|c_WnuzBbGd+^A>b(WEq4Xt@*(;rV?#@%vRciCYF^nxwiKrjX~~sj>!r$zk~Z=eeTr+qDZQ8|>&< zS8_FcMtC({ktxQ6h~YKVGj$vsEMW*gQ-rW~+6;D!KmMRvkz|E#tHXJ`Fx!^?j7lNT zY0GNLuzp+xZAE;<^E-u}&ce$c?<)mm`)yaF8V4;AT*M}%AS!ZV_n)WdmY`PV zI9>K6gNQ!&5C*Kz%}U>wFLzjbMFzM1wmSpgK`5)q*!Jf{*<=)h#zx~T=M**tmn&(q zG_|pbaqe7(+*0b&k^wrzM9KsPwo#9zpS%Qozja*?7Y`<)fOOG<{VI;M9FeP6k0KQ{ zb&=qKox=Ns(+ewKXt(Kl%Gq63R@M_i%&1j$H0sfD)gQHRpMe1HncS@=q?%vgc{_^O zHy*(nehu&6s?$*u!C6??`L#%YnC^_H%Ams9+$6p(=1{NfpeP+b)hqt4}q z;rdJEvtC>99#G~i7YSas9ne2*TwK`bHa(M@Ea%8j(r3QrP97-j-mm!8X$W64GGOMh zhRM*>>y{YQt(#@P9{%vdoN|nsw{2kb(=H}HZ8^`-Ycd1(Aq|!6YRTwiSkcM1+HZFb z9{7MzmLf&|JBH&4q~CsKYLUbeal7@0$!A5odVQp!Hsm-^qki@-S&e`P?%?>68Gk<* zi<2!*nyAHNH<#h|`RaMM(fg<(OHAN$dcN7@-tke5`^JMt2>{h?O)8eAF*K*Ff7QD# z1>m(WIIjvGSw6WfBE}+G!LQ!hA+Z%q! z?~3RW#0!76YvB?3uI{-UkucK2@#oZD135kBY}XxNpGCcJ5AI(4J>pmh!VrlDgR1ur zi$k8tv$*~93YW`wFd`*lNX5g({N8WJ95o>^Si$S#L(6ik$G;}hz`g{4m8ziJ+&6LgID!RIZWB{ao_H1$FJALs^^~ILq8wXB_ zVtb}V6Yc9Hu$uKu2|1aO3z<`nV!ioRLt#if?WsEboQ9y<)*2}7DFBaFB9jF05c1M< zQMn1Y1>e3v@I_CXkD>Y|d0%yWuZvLencVxQ5|>6y&ygt?nt;{*7yTNOE$-+DsrzY+ zF{`hQKgV;n#Z8Q+A2N9I{vsx3PcsCa6?|63Ksa~WeCH-Wy8q;3&7A9pVT6#ZhQ^mM zSNe~`dOjHS+}dX}W3V5#P^eU!K2fT4`Op-@qlzfi<9uONM)Ztq*A2)Ic&w!@~%puim+1;lQ1fg;Q5PwSNe612FW$^=i2lemYf4jWRN_*RJ0D z(})YF9NLmul1(^`(0y4V!=`M!s0dOJd3hZkx5nbW;^N7ZmyeY?Stp-2LnvE~!4EB{ zXy8C}*pr7@icLVkl>e~3&&>eLgGz%-#m+T1F`viys#ez_0;F2}#?r;D#}tkNQ2^J{ zbs!9}M-oVpu~f;43P}?a8=J}QcYmtJFnH<+rrGI2nLxQ7XE0K{^$9zc`0 zHd#29Z|a9n;InyLLr(8nD2N=w5m=jDbfCaQd}oTCGH-wkc80k(Xtlw6D8i!GX5fm( zuLkrD7(VwE78W8h1cAnuQmTM95)>J$lNt0OwV#b9MLkj0)wRKh9zlu1f|u9we2M%M zr~2*t0U}+|+~p(BAOGx(sbd7_q#~>+t>F)G7?kTOM-pl)McjeQi~hF9hH9w|LTh4I_qkoTd?1fp8%~`WjK?O`i1~JeiQKKaw>WP84h-jtS(?L*A1>Zy>gq1WkSK)i# z)zo!$9eqtq%4{ikX(LUzbllNo!)C&#%~eCd3!Jt(T^mwUQ+qL}6>@LedzjK!K4#06 zi=H(@W)Hwc$`(d6Q2+x3ZPUjN#+D}}64bb{qvFCJ_ZYQ5m#QzTEnyo(B1Vjbj@-KZ zf9S{5Bm3y@h&Vv-mA0i5@;?R(M^AuqKvr5KmLBr#*`7Z>K2|{tJPj!? zFJ~*U{PNA|y` zWx{mufpkG^{8wva0bgYV+yYh*FTk|7r}bA-}ynf z_SFQ){@0qmCU@qDf$fmP_=(TY&wxGX(}&LEurTP4lF}*>olkM`<9{B64p7CiX$(fa zU!QL^*wG_@PMnTrp~;Q49pVxC@6Q%&ajFPn8 zNT0H5-~@%rT;_UbpBU5QXa#cN%D({^63t;_M+{!M*tobzp}%*oSJ2PzpCBR0%MJnw z&JC1mlgd0sw*R?8jXVJr;F z*d(X+cRb3Gu_?yC%L+{zPN9PYu&?mU&6ZH$LgZB1a}CN;TWg({3H&Z}nEwVeAwe#I z#rmpd$6IiltD1oHU%ODeg<{|4rmzX@u)30lvW9c#zx&dmkr6XfHqY-CV=vwMeshV1 zv{X%Jt1t1YG?>)|C7w4r%smuBTFMOs)Cb61(eh-=1BQZf!VH3 zJvWcxUsF->VxOhq{coKkAVkZStGc@S-Da=X_bUjdvlW5@9866^gR3?0tah&Dmm_*b z1-mRKF07Pz!7d&AY`J>mfaMDHBx0ag>{@YW(--p1fWF*$U+R9`mtN$6p|1DsyWj66CX~BkS!)JK-Z(yZ0oN;!N6vY1iKR^6YARo=nl9 z323EZ`(L_2a7xlnbYNw+=sX8`tO!(p#-KoFy-c4-#qm2EHldi>a|t1BcPj5~_&J>! z6%~wSF|%B)+ktNT6xz-d0JR$t8M&7fDRZv-X)ydD--*!u35dR5G>v(_Q}8e!gy zI|!3r^-z3}WT|3YO@ux{kPJmr4Zfx5l~c14<{ga0?D=Mo4K=_{ zKF}a40xQ6oq5JJ>2$VR)8TAKTplqx!hLy;sbM~j>E6B@ZQBY98LNZ_%DHk;?t*o>S z;Y4CT0Xt*Y!=UMj$Ud0j%XpdH4ao%-uZ_n<=V^s-;h7OXlbGLYHU*84INCU<`*+Eox>#f#KXGPQ&6$@J!`UwMp$^K-Fpn#__`lwR6e7g@SK!v8(aiP29=|*l9b0C zL6#~*$|Nrcc3T!XTl2LGjY=-4SwZLkV;e9P2}YBJ~S;B@TqtAS(*rGb4u&VX=i0X#{u5H|dck z9U2P4q5~lJ>1M539OjHVOG}d}*d{z$P8KXVrv5&#MQ$9j{G?VWxVI#cU;=CkJfY?!KqSsuQkglrHLfQHhx_lbnp;}% z@Z`e!)T<&}wQHbNcrHe3(b+OXdfu+O=GWJ?nN8uVLd5*yZE>R@0fFuC9Qd))<0l){ z8dh%<_@^;WC~o-woAv6dr3G7te%=b>f@MpZ5{t(Y zHh@-caDN{inn>7hGmqUNKU9wFVf-@#QVM_?Q3YZARgf@4qO{7GW>}6ODiXA6xdgkv zUc8`;2wem?C!Li5Lhk>Cm{pW#6Ppbau;+`vK=Yh=oDvR%@7znCK=P6|81$PLAN-tA)YVHn_GJiA(l%?^ zyv1H7hv%_((9qC84gy?dCa@BaXj!NP2TpwQ7Wh>?1r9hTfPl4K6iB`_z`=mw>iL8A zw7K*DT`%yks5;#M2N$3Hgi4yg+`87+9xw5kI zv^vp_%!$mlcT;43MIDA46%Ow8e81+WcFA`(3hURz-Mk03^e!9hLj0TqoE zZT$T1ik61v)PuRfhKrVtPDZRm#gnz6L76o&5)Ky}4eUReXk&!5x7mtPS`iX$*gfXJgwxMye(hi(b#5&dewb#BD&1;Ny!JnfdI%)smR?icTaxiI_`kL$@2lS z04X#;l0w;ALc0+Q?k1gC^B)_wlQ?1_fxKz+QFbkcrA-hCJ(JDLjxP@Mitzh*yl{jCiLxJ`YB~d6bKScRMvc9Nh zprx*Q5Y8hiON05h;q@#+2!)oMGnEjd3Vv*41daW%9X z@{J1pNgi7j_mvM%g(>C(Cb~CTy`nMC|I<_W+3*%M@>>I780n<9!Q`wrKX2mXHhdm0 z#^nOva^cL)=)aKwE;i6{2QK4P=s5(!>dI)%S~aoe$v>Z~wVEVUD%AeuvzcPgm0lWK?;#U0^Z-6IyyQ$SL>?iJq7ePTWfSinBw1>g-`p;=8GK($00-H z%)cviDI0vveJ3gTZ>0UJu50r*h(Jrr^cCIV1v__+ml}N%Y*Gn1`F=y7=H0M~q8z22 zc(X8M;+G2UCBloq_U^@0b;!TuBP<)PbF|!u9zIT5Pueh%O<<`)&M#xf)nmQ*X@P34 z^RxqXbtVR}2wX5ryPtZ3#?J{OecQGz8WjEou#a_~GvSDT$-uh*$EP(=oPKBWWs zip(@LB5RyfNt;{}0O{1RVcr&+=ly!-4P90ijRY;8GHt$b{_wUa!1P0!G~C1=q)ZLI z5l7_2U2jV)$F0N;Idwap$}B>Je4kOS9Wu|AY${nYZ2dlrx$7(u1MvC!cp5#%e(0l? z%VCQzQPOlIWOixg*FhoVDGyCFP%N4IVkYJCqmLfq+VTJDshX6Tq+6-cVMh@Mb|#{c zODZL)s?67YU)?sgGSG8>ZHA5pl9@!$8~n7kjz;n5w!YGL*{R+e3V%WfAFP833IvIC z{#G@S`g^6%T<0nI+@FSR@mmF0Xrygd2{o#iTc=-n9uXCDUg#h z7K1FYO_`<{r%XPJ3sM|di!meo<)gM85SYG|5jK8F!sl@+>{xl)=i(NlV9(>)*u9yW zo2#Fdk%7ya8~}>t04}>tOOO(t5|K|;k}6r0*AouDqtHNJehay1uDD>{Qgiuq?umrQ z!LVfqM+x?2(=>Sws8he&3JP0}NOIXctX%BuP2@Ymo`WIYYuS_OANI;^VKo}(S2QI~ zhajDFD=DrVBTI*{w|Npa1P>0q}O&d4% z429j@ zdUkeprmoW%+z)zs`X@wUJ{Q#Bf%a1|(&!~CEAA{S4??^3{=g3jAOKAjDFQgp03dw# z8@cj*lgVTlbwigVF0r*0q)L{0F%C3Tv$dW6rxCk6SiEqiTB_jN+gq?_5_>joP$s~tSBn=NdAl?5(j;Lzc0Wo0c2F_>3gSVYph zdPj;1>)}VQP&&+>i5M)ouo?9YH<1c3aCo^i|Cc2!N?m;>E6j;}NwoWb!gdwHEK z1{qpK#PFa7^hg^2tIZB@5alDb?R8#r2I2&&L3HQom3I4$8CRCP&8y>t^WVScLtIjq zC8ec@N#lpYkO**r{e!D$r|*7QL)dgof*NAY_6&m&?Z92(ua}u*I7i1Nf6(=D;^8Ri zN~enxcjhx`7i&K}!e$QczQ+=Np4hL{=}40$t_m>O#|X%v)h$@{?K^nzvd&fF<3r8( z-_L!YyAT=1x&^V~O;%P`e;>R)ZefVq-DAXcTF#?9VpX?teuRxhLeEG; z6Olm%JaN91&F%#26){wlk%=PV@?rT}IGRK`L?YzzKCK^s0V(RB%!X$R#9z%U1Y0ex zd*wSmoErybWiTTobj6HJ_Kk61LpDovxtRIgmAhrcrlwb(N^;7n6Xt?|zj; zj|nX8`@F0Z2C>k|@st!4+i;EnvYqkBWeZ1+9m3H`6Os*U**dg5pW$$pyQN;pI7Gal znn{OK#l=+0&xK_}G#OC!6%;?F6`8fB96AKP#-(I;2yS3lg14XR#TY)js82~iFA?hZ zFFB}SE62N&rWMP-3BG|Bf zf5g4s4$6aR=nRFOM7kM0_se5#MOgXaSJuaAUbSi5O5h{<`1PmmUun5B&(4WY0^O`!yYj(Rbo{+cOdm*0Yppn|GZgF=G``tg;734k#Vwo> z%lLHQoIP^#H1Im3^s;jL7Y!chfSI}3afYJopEqr%(`7apFN}mQ@a&f=gXPZp18g7S z6(obMg+fp}s$YHT)41{C#om)>1#obM6&4m{&8w<9_X}sawsx9(%h^l?uW7BxBTdU6 zB|K(H73p=WWr+auVS>Qe(`RZ-?w++Io!=xK2#A%XDaJrGH(St*L~ZC{$la-89yfVa z%l=ZL-%gSxS`Hk^`RC32(9u=vR-i%uBPA`JIh4P^>}9hy(HN`UW5kXqvex$TbKt99 zUu3uwZ~_>yv$M0@24Y9mzmo<-|BAUh$2d6n`rEq7Rlf9hD<|i>CZw|RZ%@0jteYoE zHcJn(cDNQ*^J(pnQiLb@{_ybr#z_(k4Cf4L)TyvUf2Q`N^y(-q@^~W+73~o(z4wUh5k)az>;T_H@EpmW}WL18ROQMa544O>5HSvH-p5QI8&**~$l_ zJ-W(T57Mfts%5l7qy_;cWhFvqF6)y?Nej6&LQcgQlP3tgo#Kw9K3rnqoNS&fV~ObQ#552H=9FSwVi^ z9;h{|yx$8B?Gik@s1el8H%GQvT(-u+XJ7WX z6CkBUI#cACOV{F(`VzC}*3}040=s$exx?JSpT+Ek>UXOpJ%0BqL~*`Oc-@r<+BaWb zuTTK0h=uJhK2I41x$4S1IVK$C@%EIl130&d+w39wnLx zr&R`JIqWQA7(Crzts{tG9hoeQM0DI zGi;PkY1i+B`G6u9rT{2Z1gI6XLH<5v+%T>NPx;4?gHUBevMuO4!dT~X^nec3`P}|xi|J&1ybU%Lswl*g|GgMi(%SI4X zswOjK;UiSLO0P~^tJ!GtS*5dZ%$kj3j*xmUtq>^2tI?2z{Q2Z}Mw;>sE|tG)5To)Z z5^9_{t6x$&yFWhq|7tnQx2V3a57S+aAl)&Pq=eK^(k(DZm!xzmFmwtGNH>y#G!lb^ zLkQ9#EfV7p(hX9-^L_igfBu1U_P(xjoxRsu_x<7Pz7hzvzGp+2H}Xh>w?u)ofCj%^ zV}S6aWtS7xj{_b$_bIPVd!F`B(kJ@GCXnqj3;Qm3RvG5LvyoxB z9l_eyqhXI;C(IB0z2hBnHxb&^>O%mqerByD(h{KM|pUsD6N$5SMwqFub#WAf%krJ8UHrF#U`ppMhGge*A&v%VOJ21 zj~n)tGpSEA)XGqUaQYt(P5oI?^>&3lGgUm&e*f-cs;irS{P^*Cz*ZmPBtos7w#<}pL` zc#!$_+FJ1pS^{(yl@VO^^RA8}uL4$s<_pqXT&jBl5CyvUSYLty?r%>?HaHJ^9$*K3 zB%?(gp!I=5#TzHQ37*wo_?(#g9ho_&hIYX=(ABm@Skt5)1q@RGF?n ztU#gJvAwo^95{>gJZb^Z^uwsHU0f^kv|Yv+nf$M83IBD%=jY~_v7z-SVK%OB_Ki>Q zBG+k3f1GK5I}FGd#5yYdz~VQ=g%#;Wmqex|OOHF)VJC$uIP;GZjs@p zX=`aca2kYi%qlcrx8g*mLXQ$^ZL-eA0vt5)!9g-WHD`t z^ex-I0;=qMuMM$-R#CE%q4C@);?}0wVFIhbx(cYF^b=Wn0m~*O>=QhSU(fdbTlnb> z`ivf{$` z=j;SLf7gOR@j`A}tK$V5$Z5u42ANb@27=Ex`+}au^`E>1Yp&Hs|L`p(Ft{_j5-H32i?U&KuDt#|h35b9B`fiRTcL6PBBL)3_b%USwF8GN@G`HQQ)6 zNcJV0TQLx5s30h&3aD!hn^fU+&%xpX?pVIC-{nL>6bp=i@U!%fDu|rPJm6CH@=v^S zFSK}!!_Pl+V%Ig=|LB#L#&BC`pF7ik%gvV!noE-wI{c!>qSw5*XD(Gx$HIia3h$*R zLXlB1{>H5fSK`m)D1xwkgKOJi^+&ORac?-tF)^GxUUpanF{aKgY} z2nxd*gSOVcM!d7>>SVX=%svNt!M#-YEl(3t5M8wzOOZRX`^+y-@FQ0%RIm5}kd zIP#Y=!ibb~V@cd*+))r$zDoCgFE(@|O;8A?oiO{a~FU(Ey$6%%^7gG#fDrdrY{! znmGWWX+mLhIeFW;>^M>QJhE>+vHjPu92Rwh$6@!p-}ehQ03GalQI!J0FDaT_JdWBS zZ!CFC@d_sCv8^ZOpXT+F00X(yk{1FOG&NK$nR)!t%26K7<$c#rsRv6p=UEQDT2n11 zGbhYGr_S_dOUC9?An1M|_r+spdKRvYL&6#`YAgJzG2An5w#2h2 z+WTeE&apk15JSdz`-NBPFKqj^;9ySE7uGgt*J$R}Hpl^n+DWi>d9zKHfBO6x6-m`# z&cC*jfJ#rEz~^Fcct)q08FD}HQnW(H;wPkXwNJU>C^(K;nv#gVcbx?%9p>=@#AR(} zMz3FetJxh^)bHS_bZ8BVx_}nme-!t7hACB_5{N~Qik@V zb?)Ngf~!hR`R&OMuq8EN)b%r|{e&gYud}8IpZSAW(Bo2Huj;aTPTOO7inmMU<6elB zr{Iy{tzn}`6 z4@-K*kL?`Zw`?xAxH9Jm+4TNgSXfX6@^Hd_swvW3SNP@d)hfzLV6kAUmi)1N>WqT^wJ`LLt4#)8jIs zoz}g*y)$-BRbvIudSGnew|11;SEN6TzBVK}*RbSeAd2Q6%iZkR_T7&?Uur#1nHyuZ z;*t)#+c@1NV|ebnnn>#d`|j@To=fvBnnF|bsrXGtW2q_}tMQO~LoJbp1kZv2VE2nX zAMOQlWpqR%)eqq>Karfgh`EzzQJZq{qiY^J!OYMYVU38iX3|48u^?aDzE`oRoN{VC z9~q@X+Be_7ahA~h#1ZVpLKES{aYvd$by6aI_$+(>!qd&ImRiY|NWX$q<+Y;qJul>E8=xxf6TsvBHAoHs+mk(K~@bZ9WW~esMeN+)(M@?X~rzmeKdZ%vEsd{`s+*{rNzf{fX+1~b0q3oPy=%+Mn(r{#0wN$XiTh}3zh zG^h$xn6Zop<|hL`h)ho#X^96tN>ZFt&^bowNv2S6r4Y}ZmcT9GaX;8(Rtu>fNALJ9tx(#6=6Yl_W^tJm;^5}nDc_S-__SY zF9dSi>QTVWM=ue)ebn}N162;4z7DbUFnVn?pm{^iv~)GC>2X=z%VdGB@WULBB_R3G zhkTGX^}tg;56xSym!PRx5+BoOp7mCtD$m6iKyq&{Boa@6>0&EtXRBuEVE~45u0N&k zoDz|%RcUY?btBKuE|xdu-fl&LC6p4xE^x>dsRQ*cE9=kykek~ET)fBj5Tc}C zVZUeZ>&fLLF=EdbR}!rIp^3~8xhh~d^b{<8^P3{3_8QQA3Umc6We7#Y`dEg0xY?gl z^?)EnEI6fdddqrdmn|xluKK)Gmm{x5Y)|1MtzNxiXZTn*rn9xU(wBQYrk#OaMU8Xr zF2rjLnppzpIz_@hmCNWqUfDG?bOpZ~ivWDdLC^UtnCyM&YUtY9a8&z(5N#!gp66`< zo4g6Bi~#&Q%JW7+sZj}UEi7e18n-loSc?c(xQYM=(=vfc_nFB?FYb&2%NjYBS9afK z!`qK~4ZYUW5(EAHq#iCVnWKfGU`dyspBNG5 zTHDLYf;ZZ|5UB6T7R7qJ_o>SeFu9cAwRba+UlmHQ@wAe4GBzkAJmhc1W|AZQ-vz?> zcyiAE0n&)Gwb#;+PI5@+cj2CiJC`@MC*2>FVt=1GJY1g<0$W}UK1~ubuFB6yO+BWB zPu#eLljDR4)z3HkkNxw2j^>Dgq1+T?_{^}20n5k+yxiQ}d`ViXHn;iYCWNTt>9+4* z3bHH->_htiLN?CgnVklJv=s5Ahnd{0NJY-?&I6_RHSofVNVRNM>o`V9&tlTA*DQb| zqnV$XDNZ6~pEh87tfXgzTh#Vz*Uw?JKsJCT4avYtNeJfn4i*xe-yy_;g%0gp1XmhM zkZ&AcsRlCzoHyE)*I^!#HrPa^2Mc46>x|BtlOS6Z7G zSESR{Un;HiH3S*R0FW}{sY}uv{Sg{4;RmcYmc2l4owR&?Lvyq;750eyINN1^i%_5i zwM_RjZBX9<<2zjrjDZ6XxruvcXInGV21T0M+UR_UY4*~xcCA~kpoRP4GEvnTHbU7O z|NYqCXN?|_1U;);R$kTBXJVyOJn#=A4_sNAm4k!ewJT0Kv35{4(uqWpahwRRprfj> z&So}hXC{8yfH5@T)4OL*lf^uqA-NM<%n_Fi$I;+8Z3zHVBQr6?{#;!x1H{y|DGtiL zcypjw)50w%IAK4t6A&GNl(T z@yM>hx3C$sH*LHx?pRL4+E2Qq(zAu*2?nN#RqCgvKPz1oF4TbrPLsX*7Zd(`3Fq4= z@HA3Rwr5j1J4U4kqIWX3)Dp@lYunF~AJLQZA^oTGYZE7NJ( z?d+!;#(6Kz2_DtB;as}iTrh?{H+ps5cY+?d2^QOy0}dCTtz@dv9`rq{S7YOZd}JXG zZTD(nr2epbor0y!!3a{`*Jm$zjm{*8*nBU%t&3x7*3|R8I~Xd70}ii~2T<22 zLyf8t^jkbBPdWHV`6EP)XQdQ~%bb{)VH&3}R&4F^?Jj*T%pBn|i>Jo?0U1@HaI1jt zj;Y_2*g&8LoYvzyK%cM9kT*nV$yhP}V=#I+!?fmxm7GQ>3_mmrn~`r}dRnn2nM0YU z(+jnhU}+WF zKJ8T>{d1IgMB8?2MA7;Qp_Lq*FY7tfUUX8#B``{*0@x{ztYy7XLEvzVZ33cHYX9H!d!(JU9OQK#|K`S&PP0xICx;8BeDORI;=jU*LX= zG|1NO)fRlZf9O}Wv$PQu3T^m1BIueWb2?i6Cn}0T>U0>t7~@awiU5>=E|4cfxQ4F1}AwlSmopxaZZI)?1ecvZ)c> zqB==8C+8fQ^Eoq*?<6B)kvMBuEZzkzx6Qhluge@O^t{ad?+H}Oh-Y(K$-lh2J6X!j zm9Esu`v;cGq9;#vG$;#6jgrSvLXxesRx>?fda*!000$HqOaw?86BC>1aK zAmm4h`n?qO6HWO(@hlTn%6>85xZ%@VCrg$+N#F<9NgzJ;`f+!Ik0iZ7V6#pR0806XPna z4JZ=9i}CncR|eCHEsIZXxh+&Tei=0@x~-KoV?!N&{;k6D-Qx2NInCzO+TP9d~#jnZ9)1p*#fn@!wj zyq2NQYef&Yjz9NkXB^ff(J)lFYpL$B3^3WUy+pgg#KhRW z43M%dW~#W-fx`T;;-=Dkg>>Jhr?|&zW9d~lKGQ-NYX=D^>~rkdm;~QHlnpO?=wxx= z_N}H}iIKrx#0M|sGsY%7x}nu9*K?`9b?n$=d;9V>QW_!o$+-$f{NH_wOmN+SufiQL z(RJSrp)P$ZGTN*Rur9PvByUXh0I|hn75Rw&ePuRzXKDrsz8qkwFSUm#ZW!yrnZhuiW67#+b z(bK-oO^1hERi-Jby%(i-Nf?CRNF8?Pp6ZeekKIsbjg+U|IAI(?p+j|3t+my)9=C(F zPK7XdI8Jfwe$PZ7R#j{b1i&A<*A^mm0 z*3EUwX^!&A6WDbxv0(A_^&<>W7{U78WR9w7%tR~*0sW~~6s zobfvihXg&g@uQCXAJ)cvA#(MU@4)0_;a{-FS;U_ry-KRZt|aHv(odY6(=9#?!FUN!+0lH22|IC<-y$=bhU9GG|NOjs&Q}l;A-K$kDl4_1r_LU zHZGcj;DhX5k_;r%=`^KEB;46!>))!Yt8a}QtAl+#C3)EZEooA-2yXW-jGtnwa( JC;$KeT~g!P9&j z+OWUqu;9)l#Zi;_luvboL2iOvmKSn6TgOnEQ|fiRwxrxqw;)~vR>XrZZUTHlF1oL6 zTUz(cSbNU|zhRdLe?@OkWNzchA)_+ zMJ8@zCI>}G!fe+9k-NcR)FnOe~EDipH&9Zu#XCnr-cE{c1uxf65^S&z-0xRo1q zMSh@(kNqA;diG~@w33#AL7hUh`6Uk^p+ z-&u^6_}p%+UB0;Aj(y6|9l$B4%pNzRitIIQnK&ryjfg8QDGF(B<`E9vn3!xk=9 zERi1^v@->%rza0K>Atu}@as{T?8%!^i3&Q#dgA+1}whN2KxY!?! zo5I)DMsOTINH;~B*P}%j#MMb2w?iJSqo6ua2}l`$7YhbJKB8fvga-}n^o--j7IzeO z^zGa}7bZE?H%&yotI%2iGZZZQa7SazK)9GYt1|GV(goPIH$~Tq0*~2>WM^w2DpQ>> z32H=I%33%$N^Q!Rw$Prhs9%4Z!@#0#x&iwb(`0_+9nMy&sbO%E2v$Tm$RdG(A7lW) zplM|FlDT=37K+X5(L8r%fDXqe!htYijLd#hR{$KB7kxM^Y%sv7Z4JO>y?FO~{kyb1 zYOg*RbowE5r<;`cMi_MU0ST#zTq0lPlrnC&pQ4!}EK<-S4)611M$)(0q6$-QZ{xR% zoy1B2d5I{jQ#43ZkFTw`WzQyYp%84~vPgkynkY|2$ZW6h`=I28%Wd14(q$}i$hrMq z%E^ZCAw2jt1~14=5~UZ>5(gR8Wr=a&>&=a#xxDq6Jt95NOw0#48yT-$jof~&2q zUC!L0&p%U%95xW4bD(GxoSUgrw1iqlYIehEr2B~npg$-Sev>>|TaDaYo%SD6LYlj$ zr?5la63BEhb4cvpDRlbv0}! z);oL^X~(^epgY3WskMmCt8LU8IJk0fEOYegK5|QD)^rSH7z ztS4gOh{2mkVRg?^IJFp%HjVBz=vMzE?fCn1>TuQQSL~ogZP8^19t3F^<%RHTyGjp5 zXQ>V?T1@eDX&g}ATgtY;VIoJ61OTx3a6=&jc|Gqk1J*5zW9u8!N z86xUZp@oM{#}BjVUB3a>j2~S008fA(k#Hr*YnQyNIFQ+uuZf7b(VN>fz<_o|(=I)% zq0p~kRH85(4*_^y=5;#Q__304fU5c*W2sWH#Weg3>OVE8@kF*{LNos4>CLQ}H}M}T zP_wh(VGNUC&XW~H2?Hnv* z{7i&&6;QK%LWIO%<8sv_0k1XttOB>PhLaUejL`wiM~wk@_SmaxU)A~uo? zxplSl-1oYu3FFvpcmhO9fR)MPzrFfg&r=XG#Z#gfdtlteNs6>epD;rhEfzO9*C!k# zn9M#PF~-oQv$scjwPnuAPKH>s;=!{%&iBNx6AN{Vr-s)D6Oa3Qft~F@Ff$S*pw!e% z_v6sq+@qVXzLhBwrt*dSggYJB51KiUkk|^{_?TeFU76ylD)U)3(8nF+dGK7sb5{v^ zuOJ*-J0?Mn&2wf<7!e^i@>`u)OUQ>aa9o)rL5==QDdUtgHnXO{!syemtC&tC`l|rg z{5&P{Bx1kax98kYd_4k@8)QlV4Ga;2$Nie8vVPZABtgDo_Sx;DP7Pa7DYp({sG9*Z z4I>sxTR7|%SK3g6bm4q?i-Dzuj2uc^ULKgJkJiRAmTuJ2^^yd`uHT|ziz|}}7B0NM z9biL?hu{4mdLaSnZ5P70^29-}?W)S!VMW_!M;1NH{^w+U8xX|9@)eNP{d9XW)OB-Qi8(tvhmMI5Lg$5+{w!geFmBnB&-*HvA+};RMbz4F z#>(YkU#>x)hOTWlE3U7v_qcZKuzB@dZcnW~&7Y4X@%TGwrY|2N#lc~=-hIW`9qq)M zg%mAu{3>p`P8N!uvX~2R1~NPCy#k@(rO=`&ESnJCY!*f1c0C`y-U|bxT%ma2*URJE zv+%n!G=WH^=t>pU0CC7a3~l|s9r4<;8+Ui#*u(!Y^(&j-GeWeqtK{FGr2fG{VDidY zzA7Ca#%eYKoZ~mJ{iZ*%Lhs;>{`TggOb;#oiTL*x0V54coIv|JT}Nl$?E3XbJW4ba zs-dj3>^lFK=4eNu_ZN0wnC&WqE7;%u7wfA8#$Hu zBP@%$d@nlgfj=_!-~Wv^eo;ObR#93YLhpFa@2MRM(!xsmPE{i18@OJJP`7+GW&{(a zT`xhIfWLbn*m5(kzoGxzBp42?H2Q%70O_6RgK6FyA`9@VsA-Xfc{OvQ3x0!OygT=T zGYrY`NyaB8-kVk&7IyLRRu`9-?@9~jr12~(_p*G=Pah!0Xt7^qDU~S+onZtB6HLHj zQK(bmcNn-rN8fM}M9KR%!M*p9WRm0+kx39X<=wbGm+KrMdLQN*h#WgfnDG~#5_XgQ>+u}Xq(G>;t0W5GlouY4@O7Z1 zq@-ApiD^upKRxnN-v)?-Tq&ngQ4uSJ|6arnQp+a@kT&#R1Jw+MD? z;g?riR~-jA7;rX|i3yiFRe(5lc+;$)qJ@)>snWK^Oyi2;K0ZEdhDOS;q-b@QGLH5^ z3dK{DrUF&HeLOUBXe%GPOC5@L|zgYE{T!#KR+}vro4Y%%Sb=*>b6*wWwIz@sQm_IR6$ywWeO8w4RfesbATo1316W zgaTE2*ZncmC$Ub-FKJxk4o#I=N~Q29L1@575M)pkRniGXgD~!|8PL>1s5|tH-I#W^ zMu#zF$(rMIv8GSW(2yivS+2m!qDX4SweHKukHZlx+5w4`nLnsGxVa~O>r~rJ#uTUw z#WNtUvUGHG-k2q_ zUEa^}+!g9-YNvpy(GOIB@Z#c;vWX_pex5O#E>qfIZbTO(aTLz^Uo#he_AOuP$Rl%H z8>y>r2WF#Z{Y(PE3(q6StQRA3G#)5!>JHvi4$A--Fyb7UYUSaqH(Ksnd@*B_E#i?i z=!x1^{pgX&*-+RfkIKKh#=+Kt(TXQH`7Zd2Fkq6&Y__3+4o&Y2D`hHu&;(LjS63HN zR9r%D;p_~|95?E4J(>%`_wyR6*c%P~{COie`|k&FBE93E7!Q3dVxK&}jh@NY z>s51*KVJ^Dn!h5N-qTQ)6|l9uFtiQdq>GRbCxrD~aB^^#KE4E?#T41;tpBzWdOj=+ zgp**=jsp~xmR`T~TuzHXF2AmA$pF&;W+jGxe(fKAjIB^7WKOp4KcnAnnSe)++yr10 zzx*41zE8;6xdLPl+oK_b3c7e3B*{=#EV$WZS;%SdJO7igk`q(=aQa#B(x0>K{$gJY zGtfXwOUvYVcbJF~Umk8O+_>HZ_(H5FDJdzZt*ss4Z@yFZ{Z6aXsB$ibvR0hkYMhP8 zjnKx>uu!CQ>ZYiqgjBcPi?z{i;pfpa1~I6`aRa7Pg8VNE{Hr8>T`Dn(ajQ$P8{xQE zzDgI?C?y$FYrcJNiDKq2ZM*uyydYKyvl?U|556@Yzb9;e>L`^i)Cv! zHfvEkKdO#lM^l)+tiCWqoIP0<&)&HEtgf51gZ0y={MYlg3({GtM&b9D>wZID&-i#) z7w+6~<_aD^9ja6rE`?04YUhcZS4n9i+Zi6lyY}<*pV8E@snfY)B1HgM%76Hvm&%EW zVITARH#kfL*!5@Aph@`ZzCh(%_Jhi_G?p&%6Lf<{Sf>R;PgU(-&u?&D8=iK7+yTJs zghWRAiZIp3Gz!q;@p9{OG#*L@2Ao&=#7Sy4Pa~<+QcuyJ-`U*@BFc)vC%|AUBxB=bu3#HcPFpG%V~C=Cb1E zGwzVOZn#&Z6s0ECr7S_zpV{x135IR^VxE_*DFb4P^0G@1%hNyF8;|$==$j}zwRixp zDb;nqgX}q>UykQ=g{K7ANg`O;T>h3H6?PsmHHrO*5+xR0GV0eYIGn4w^c^`(3SvlA zJsl-fonBH#32k(qpAQ1_RX1}(0jEh=o{=S{MLn)h2>K@n2+7n`mW%N-*Zgm z&xh%eJA?a^+rbvKdg$gSJM~C;4p99@ap1^!-;E( z|9SMK#dSlYa!yCn{g$mmNx0-U_yWtA-0)xD@f_18%2q87047Yq-0$?-_ZPD0HPH=R z*L+nSzIcjoTx2ftPLgLEA0Ih;$=}7v^`#O%J#A0)*kQNJEp671H}$41eG0lux)b4I zpVWNhA-Rj~r3#Hg_(`1-G356+MH2Jwu-SACfNy#9T zNtm507oYBgf>be#$oq@ui`SXZNx}3X^SaSozkkDyFx!|_dJXh?y1Fgrm8lFHf_nZp zs-cYsMokm3j|M*j6G^$xi7JOB!p{xp>xq$Kj|;N(6Bih+9{t26V4Zi$k|;VIzt||x z9J?<;w4Bt%T>d%uD^Gkjw%)D%4ZxF>WNhtoZkjVQJimT^rur%o(n%3Jta5O$*FlFJ z^9Xqtw6qvLJX2IuM6BX^wv1eBJTF1u4p3Lu)(7;x+}!N;dzy?SAkL_G>FVmj;U&uR zuULN-Hmp-_jJdU;N$IrvQE|f5`;(`R|Y|y;huCAo!K1N0qC|C?kX?doBz@J#Ba!~&3uR$ zg-m0>)h3T!s*{nGMSF32s6_T795qm7niHlj}>=Z(Kcc;W+??hz?v;ND-ho2e41ia3?Yn zzPXwCv|eX7y+PK<_inyOGHpR%UMp;mKMf_<&;_~~S$Eg1>-x;Zv{$=dHpe>nIg@%lYD z_1m&>@1irTJ93q>Ra`d5QTE74U(@2*tM*m2#3$S98paqNZ;H%u8x6bJCZ1L1 z4g==*A;tSu*KQlvh;%%;#{?4oNi&!3Ut)jA@+_mv{tXHULXpmdx<(q4j$s7xJO>9}5>+ZW`ZWDC4qF zxbRK_j3UHiJF1&0RsQ9DS~J9)Now-D)@Fu!)uDzsNp6QDAouwkiB#5v-P+MhfYj!J zJPM$SvrdL)F*`qxk1i|kKYs2eI+M#m(u%Z$n~cMsM>UPc3735uE8t)U#xnS0S%*TSl(TB6t-o@&>N9s>BSA+cA#Fz`RcT`yX6kFa2Txte9hrtYjAb;!!~XO5V6?ZSCWK9au^ zWL`H@0+2r+Iqi^U8e_SBWHqbywyD??=dlo7!t;gcJ} zH*Zjn$MpA~eK1cS8IhZSk{=QtJjC~>D@~YfuXig74r$y|CLEVN2(ab=%FJ(zrrVXua%>}h;(L31p|Fn(E9|GnoL^D5v@F-xSvxGTXjLOO@+t&76Mn#1$PslT$ zwjTR_Ia>cDNdEKfqEtDfbwBn)t}!N7XhhRSzu2m#vt5F!ZhYX92YA-yg%E25<#Os?|RRfOQDW?_H}U?DInu^n4L&x zJu^(DUx$`6%w@qiQZ)5Q2kBLJU51>l`vmsDlN1qJ3(W9K%zY#NCOkAXGb6xvCjCK9 zUH8C9%klU)~hQDw;1JmR8 zi5S4u&4K|#~A zU-t8X`y=VIyTy$Ma)AETD4y zoc7DbA+1{FoM1H*Q)CaAq6EQYzDhS-QE^TWi9w#XLVrf-D4xmBr`% zPrGuVO6f0%jq-fSD#sfE3}l+q)RZ2I`?!M<*@vk^a)${|zGaj<#63@T>Dq&Ug)l{o zgG@B?0a-Zo;I1*t$})aYDSAf0MjvJuGFE6#Dnr);S=!JmejkM=G+zy?B^{yzvjapwq(!qG zQpb9eXZAGjJk{1R!3uS$SI#8|ER)Lc=lp7b`XsSU?$AYAf?gouF1rJ-0|>r93!tG*5`S`Z^bwUj z?I%Ua%;C8SrL|bcd#dPMkN@+@$??Wgjk|u&?ZRJSaEcta2sY1I3(bqaP%b9zw~fTx zPmHr)_?wEKD6lUug&n>bbvfnx$;chgK)^C9b#~fY>dJz1juhG~_^ecbem)m^%@@4H z2~=h(M*Qo8Z((5!F3x`#QkUwi>~T@Uo=*q~T^|MBn60tXm6}-GJ^FZ zqmsaQWRomd81Jp(-((L;P;5%$4WWmt6HzqBFm@!p=2n>lgsrvS&r~==|0HZb>buXndwUcZOq&MUO7W+ zkV^Rvrws;!s4N))DB%?B={JsHV)^pN?4sS@qe7(D7rU8EW^Y`4r z)Ytc!va(M#1C;BA2gKmkVZz5&ASm&BK5HVz3hj>r9HK=$lTXR(n`Zub-`<+++-?`B zFPQj{pg^sU9y-bXSxmNU0PElj*f0@bY^((A6Mo8m%Fi~WEZ1>Z+?Fm zz`*xRQTB4^WI<9$2_aQ!CI%jsLIezc?2D+bTl%ulXID^LR<`KT`dW~Lls&$;Sgvt= z0Uz`SXVZ()f2U;p9($s$wwAtK=)y8_h^he;kA@Y>$zv`Cz|bc1dQ~fZL$cPeP1Im+ zf`Nyvbvsh?wUpZLGEs=)tSvj#qRw`1HMHpw0n6uWoCYmSUI4KkUxk!>wY8~l9y6ev zo3LVedAT7Y7e6><^k5>J|2|pKfnVE93}Etg_eU^@1!24%g7yHuCc7d^6!<68Hf-z7 zpn#ewFE2k<+Qv+RftV_4<)MD$wsHX*F||&$7wP|URCHoo=Ud^ht6fTh0eAI%6|N8O%*5Z}mmr zTtKK!+0APwwyn4+Nl#kW8H)mF@;DO$b9}T z1SgyvW-5xGNeV#fM;^-&Tsd-SIsXkp6AVWP*DPI8uAKVnpKn4F+T)bloK0&gO{G55 z;PX1ye?478wUU9huNu1~&|1^NTSAS5-VCswJ4QAM0-Ni|)Bk`E<3aHD_I^+2buHnc zqdW2jzRs#Y`|fCAzhLK!Xau7`TWl-Dg`6crU^-4WVc9By91zZn17 z-I(|k;s6+MXrgmy+56s~m6c^;Dr$s_hr<#o+f`SPzZH_k(58dd41f<2Wd#91XsycK zJKiejfc_(_XbE4ML_$feL541BzBqN!F+=1Bf6Q8)szKZ)+M4imBqgek5l+8qyLU&!5~b{BKoY3JUWL?}_$Sk-QaSR!ZJ0-yo5!U<6eB z&Ft*#EfFxpm`>fTw1|>uf+IbxjKS+)@u^EIk5ZcU)6>Z>_S}G7?CP&9ZoFAa-c{dF zh@4=&pcyJ#W29JLCsmAEnpeT&Bn%mM835~yAJPNH2o2_!jsqECx;~!#<*<+h<3*I? z9bZU&-%di>L~U00IA2>i0a8|Mi#zGN9g%pB0a!&8&=B^Wc;@BvBRh`cOrKp{YjX^t z@UG%Ll851vuZAdokU35+{^E3&lsqOh2ZJxwcF&2|F}Ho~$Z5>p)<%Jf6v`8GLPR%( z-7Y6xu;8GHC=SGWoFJbRS2*+WZ+-pEF_V5{84Rp*f=;zo`HXFc?O2(Zeu$j+a#iZO z3DVc$`Y$y4^&V}Z`RDU@^wN5^|Mson?+%E)u{t($* zUT$MYiR0|sL3&!ux{X>_D1kCp0J2|Jx*Obo`^C0psq!hb{)Z zZI2e#wo%DAn!%jFw?{ae^71_WYSK%d?&8s|?C|z>fnQ3c&m$N2+&|EyY_zo(D6>U# zJwJtqK%-7ZQT7WRGo#lBDOZ*+1FHR>C3}9GW^-l6A6$pqdvPoya1Z3#3ge{GQ%MdZ zFjBh;`}v$*oq53{($_HB{Ta*nklFb%&Ff}eQd8aG-)#DZZS*Prhb_Y+6ePFox=^hd0#-l$RvqROx(y!&braGRQj7! zS)yo{!(o$5#uWg4J1qotlT?!}hA7@7nk{2ho)OWq>D?JSKWJmZ`r>p~vKO1ZrUDX& z`f~SUW(!1w5$R*qwQu>^pQPt%ySA3?pNLj*f-DReECE>e5A53>u!PBVTsT3yrqx(> zh#haI!a{mb!@*UzL6iDK8gog9r-1IBpTL6e5U?AJNFNpPek}Y3gB2Ak>*7L4hciH@ z!8V67L5)T=v8Qs(_bv{%V+isQwfKYyaQxVcgJ`1s&Gex)TYH~%*9ztVdrbG&OED@tC>LrQ9hWyK>7h?+vJgz^QWmZ(?^0OfW zfz7DVqeS}oCPy^Lg#4mv-G{UyMo%h2zPx{%8Fm&x%VH*=0X_LQFqk#-$H6Ha*68^T z!S&OnGFMCa+i3UmLA5qW{k&`rt2~)Y_P4$3nI&rU_o}qJ>Obivi+(y2(@2sB8kjDg zIlG$j)hyVm(0$8{*Rmr8iF^fsu?~V6m4MsZ-@r^X2iABJ6DtVl;UOZ-scg|hApBR} zm`KEWbaI&ga+X9Y{g134V}uoIEmuzco}ELu^P(XJya`?}S3|}crbX+J)u0cEqOj0I z0@0;KU~J1p)7{-YxwDZpedL@8C}x)AESZ(n?)Uf)Mn4+`8JhP=X;bAghqe^7DsR=; z&`xfPPa2o)Q=*=?mX@Y&zTr_T*p!CC!2WM!|1ch5;qcf(rVrkv3yne0^-spV>qmrs ze={>PonpD)`qlFq5E1Zy3xx|8vvlGmaU<2v%bT*sKb+3JUrvSyF6w7tpakU(9W?0M zH^TqFv8H#xmrvp3%sCS~nKB@c^%$8;-jOZU~7))O(!0peUHk2CiWI##M`dr z&4%`xNoi7AS#iphE9F=08}l#mt!=)1apBK4_`lPJhJ?VT4P9VPess&=VE&tM+XD-Z zfNyg5NNk2CXz^4{%{wO!KJbY-&i_`Nh!l=Su0hYF1m4`5L^xFU_HNKKlv^DgFpJS3 z#}4|xCcvtcWzRqp=>=rU74JC7=h7>B3!bl=f$39GHwhlhgTYDdr5=Rod4IBi3G3qtZB|xNYLp^R zl7eo401(Whe8@ql`WPw4co}``3=$^o@m(=8H8nkG_r5klfP=3EU`1WSQ>aHtS`J&_ zq(a`IZqw~kib}>#EGH)y#n|g@g~N<5^WUg2c#0rsm}Am*WP9Z*1j$%*ufA;r8vvk( zRkTDwki?(>;NtfEnG!H%ZO^D44FBKD3%o#jFcyK7gvBP{{yis96-FlD2du&*L7t0+m6dV#NE!j61q?J0V78jt1ahnsm8@oM-EkhWV4h%4 zpGQYV$orqaY;{T{6b37! z`p((1&=3X~1VaGGR^r|az=C!D35qwYEh?hSHDGiOGB*c2;r$7<>0kHF{@eSnd2Kjf z<#5CeX4TxAP4~H>5WVceve}?1?2#N8~0Q@Zg7` z2R?k07|(h zBP2s1Vfx4|Qib*u2-h7{3Ii1#6rcCc?pkg2h4#CW)62o@-JNbwP{Y;Yfw5R6617GV zEXFo(y$0~u!g2dU^H@~qG?Xs{Q;#EI^IoBhl%feBDw@MYU3_PCC(%?TtYk#>sxSCA zaHR0AI-ckV5FCZQ%(wjKth&0Vq$GmadSq0D3>*SP0~UsQG2%p{!3LX9Ky#-V_^VOr z3D29?!cRjEoM=iVMa3^1>EkH3cW3?ohEoB-AQPk$j!$$56f?9Mk!u+Q7~$c0EFxqg zq%<4V=cj-L1J`&GhjJjCgn|xWl6V;-aCQA+`^pPax5OB%s%qq`pIKYnA&3Co6JYB{ zYlj0)xWWfm#vcH4ZGR#Jq%T1hnrd)SFlN25DztbP{tdR8$bQX3whhUx4a=*f4L*!g z6zqTq^u^w2r2gozKGS@$pHa9m5;0DYvVYBj{-mS7Wa2h3&4SW@1B|@)QvWu7|4x)3 zd~tlP#o5q^DXXYtNBCWEtR>*pXAeqcuawFXAoUN36;73fkq3ti?UhVtpnW)`03pMf zk+M)T2Bl*G%w&qzY)M1~zRMMxb{LUy5#Gja_x3WyqoY5P1w5H*`TP6l9{-}iB8Yt| zDY0M+Cyx!1p+`fy@#baWi-&~@_RkTa*cu6dZICp&_fADB4s&T!uLn04i+{e-^lNeF zn(Db-R5Kvp&A=L=9}eCuC@32Xv*f)%kgsFIw(fR`ym$)q35%j%0&VjZQ`qm}YKSb* z9IELmfF7rnI`kOZNiy@Dp@`CH351%;)H7m!LdIv;k7k8m3Ag3o?HnAarVi`NHZBnR ztAG7os-i}XLWdfc$FF(Y6lD6DKl^WaziSO6vs|xCQ4Xn~`SEP|#u1ue3-l6#OIb|~ z>&F3DEI1gBr6bTs^X0RIsW%Z5>)Z&yyIi}$8)_7`nK@X+99NtYXUgrk~+gdvA#v?G;4Iy+=}@#oi+=YvN)BtsnqwX$zZN{%!x=@8MiwoAq>$;^Wa# zYLAI^=I9n^Nk>0dO-DuL;I8?+QNNl&pKo~`7O9`A;O9?=sIL=U`(m(xL%|nyqWLOb zqH$R4uwT=^HM?B=^v8U8(vUBlqE}Z-CcY$!$PiF66;s>R3tjK#@wuT@r%pl-SJnN7 z4i(prV3O?q+mW>T{vt<=f^q^pG!k&wvR51fD zMylZ1kMtr0TEji)GByq$T|tGf)_*C8xZiJBpmhg3PMFOH*JqCl#XrMivtDZs6gTf=*}k`$h{G0g{`&g*>rF)S zVb|9#C$>Z<>SOrV5&nCKxn2)jkmYp_ zxA^6C(}T#g(&TutqGt9gp@7`|U)N&#kcp5ru62Hn^DkGr!mw2ACy9b2k1H<$L!P)D z13tSY#aIWgbW!um(4NhzZ?hIF`QnxGBOc;&^-oi8b&qP}pW2u=F-sw%gyI9pG8kcY!gH&hDzk z5IOEb{F9IE-g9$jUeIM%W(*CM9NPHMskcrV-f7_f3^*m1p`k&155@Bxltg^!OGmU+Y2(BJE+eEy>Z`0pSz0kTz;LL<9rMKCw8<)38a z3=_`^>qncTDJ0-}DE_fw=?`j=UYSd&^(blbVMyg?9F$@`Ec5;!fKC0zFG&}V-tYFs za^7D@4@v{t9I`d?HI8qk1S?BQm;+B2r$hUDQB-KDRcVVNdS61MN=J3~Z+&_FgoH_f zIP%a~$%z^;<5J+!jo~1}e~>DS=3!+IBcHN6 zDlZ2Uqr__YH|9sEC{8oVMuRSB^Ywniii(D&NgQ?a5(+7VdQ2d35#_Dyq;NAn z*w(3?h=ot=a6*B1<#*Uf@Hrtbu(6zztE2mE?BbnGUxwYf-V*mTYfMnQuW?K1Oxd{aY7yCuvVfBGg>Xgpxb zx{Z^9E4Ti%%T!+r*{-s_eG2y}thxEwcFtDy?EKo-qXKzVc*gx88s^u-Ab@|}5b?d_ z)0VY_^FN`mi$uh}6-LV$48rUY63&WdoVzzxb@*KgGbI3$Lvy}8cesM!@2kk%7zd+v zuk6tsIpc;qqJeE9hs9v&-X!k#>y6f$;^GY~DMC_=i85y4hcXpPd(FzZa!yY4lLXRo z*@q282G>q#fhj%z(=c`wrk~FH=?L1o18F^u%_s-51-`ilLP-h=ljFxYxDk_*Zuvsh z1dX}bRjhakl&v#NQ7h)e6PAzqINSfc9wA!pmxYN!d}wCh((!^mkA81hs%p&r%yyN# zP-Q$?GF^r^Jw5&Adb!#2OFcy`^9D%KR8dhY#+)kYL|iH|*D2Xgpj0w#LssimwIG2L zUEQ2_IWZ~m46Jz0RiSh4A7(V}%)%ujV^LC2*d5E}*KW7lSl4w(Jc!ex7(0rD;M|S4 zFBod~=dr)zpoKrsT!Bf!CWtTjsR3 zacjuH#cBsPtDxW+Qn;MI#qF`?9zq}Lov3=e{p3j?`vp3%u&SjY97DOlxOz_E8W_Sq zW_f}t)izZ;b0p{3{y2yX2o9p8qPo(cPqi6Nrh*=Wge6`A>*c?3dCm2Mf`w^9Mc-ro z7EwLEzD=tmphU}x!jk?PIj_IK2#4+AckFtlz}o4r0${aYuGSgHguy;gNIBNE85(lX ze-S1faCn}fNE}m?k#XvQinChuw!=g|OhoW%r}mu#9Gb4Z(M&2&sfTKto14>26ik;w zF@@3*v9vkDjvTo%Q4ihtJjT(v%PJ}p8Cf&Nt=gIMj>vYeyf?<%jw}2dI5|`Dj05FH zGlnfb$`8dYQQ#q=?BKBEklcxtBm7I^z%govF? z-5D|bIVDdYGEat9B9@l#z82vw-y}Wdw8fBgbK?e#9Y7HsbWM4Rukaas~W)EM0b8AT- z+DV2T46cpDAdzFBqq8yS@bOpGw_72>&(!oPG0#&bOO`KQVVj;|ustSxn_Zs@7(o{9 z|F((U7s(RoTyBe#x_s4-t2`Sl+M9cb35GlZ`QG z!1Eqlo~BNUsBAK8&Bl6id_36Fk@I22^Ma0nne?$EXee9Y`MB}`(EkArZB=)`tfsB1 zDz1?G=zZIzB_gD*>bl30%?C;xZxnqUM!k`Gjhcza8Y50NjeL6xBw#iGb@<$!lJ$Vi zRFD8Y5e=1%1iR=fq);u*oOFe}*w)v){BqN&^yFkF-3;--h@5URC7R6f!F0y&*fCNL z5Rj2|onA`dQmZ?1J-D&<4sCqtm@@Xj2~(7e1mwnxQUE3w5@q0f5f>T1|2?>- z@$nam8Mn-Q7>M~G&q}x#usCTf_y|fIg2P3^{uHnD>q5)(U@J)`V%)Y3;mM^~&}U7w za@^y7Qn%D&5^MgT#5yf9DIFPw=iaYu4WTxu5&G=BCQ|$T!m$?wBSfs#|-W?x1ggn$Yb0 zyiiMbw^==~U#nb^kbrJ)^7^#AmL?`jT}vA)%^B2PjygL#Yn!XaCa=}U@WPnGIk+Ct z|5k9l8U8_n2CD`T54_YJvKj0w-n`OxaCLDxYoAQif@-aMs))wF05QLw0IO2xm1)=7 z1xOH^OOm#B`TW{@9r&Ivl!GH9yL!)CXnvj!Xy^15E5-SViHUiUr{ON2+_9}lw6mTp z!Ac0q9uMrq6Ul#}Lcn>7@zImJ4T<^&i}^K|bN2rooVKpso&Bw>33%0xeQN%ds8Jzo zYVZN5slBv)najh2cC{7_AmPuSUpFWxk{3lvNsjDa$0YFxX(3gkwagt-;|B`@5%>WB zlJF6L_7?tIz;NwCq$h}Xokd|qTjF&*vCD!S_NEGW3I(!!KfWck;HB=?s+@oIr`u{o zbqPKt`Wvds7KbMPDruktqFi!dZr6Zr##F3#_O8FUL?yA#t*y$A7JRYkz#C1W&)w?| zv|Crm*8R3Scz^!Ok<1xSZ<6|V$AQLZL6b9Z#;7a6CXa=ZrHG9cQOwPny33>`I*!Z( zK9+G!;P^GRo|2vpHnzAt$cl`Nw5PnJk2LzN!kW5@PM2atJ!zEso4Utpe{%CVw7|1$ zJ75ISxGc3S_OaD&Z@*%XtXJ4$q2_cPK!wKEMx*1XZkj9kF> z#)tcU3C5x!1RW3~&c8v*7%(0xoXKUwCyG^cw?#`*-6C-hz=LjE@ltTR(z{*nf3C?) z7ZP-Q3mpoImM>Zr_a2U~#OgEZ0ZZKj4>uXdFZPN~NOOH3c^6&Dt|I?SPhS`W(ALCT zcxSAD&A({Jh#|}7vhwdzX1q>D12Y3qx|5mv_-JLu@}|T?!S*PP(kJIXEd>^P*l}3~ z;D4iquS5TLi{`_h0CokqNc5YLfL22PPX&vCObHrV1TjOOb=gVoV;GSpLdJJJ8V364 zKNjgz+IvM2V?+XXqU(xCWp`diT6R-`dTBiF!_y0ZB4y(T%ZrbbkxAdcw+E0J%YZoU z1D42C$$~>xo~h}ON+Rvs@Wbx^<`5TXT?qvfN2+R?{=Qo$6cP(n#=g~S=9vm@G36@- z8mvn2pyza*?RT6TbL?#(@-)8C?W%_(&WwRw2X~3QlDCF&qC_!wv9jd&`^;$L0ZW_D8k!2mqF=Ih)1}H?d=@tcGiOi#?nfTEYB?h_%mQMlSdwdW z90gAR0&nv8HEkz37&RmRMIr!f)TLH=P>0M8MBGakY$n>FCwJQ5# zT}Rhb&VGI%o(VU|0h!(i=205*+zD$|ZdBX8M&Lq<%&F^vevED|p2}<{A4^Z~Mh}%O z=InbCY3RTfv)XS1@TFT4VinWY_uE)NimOug0Ym9l(YV0NE&$c;KE!<-nsq%hW8Qi;*t`Mq|UBm_Rn7T?5n2i9-*%hMm7CD5T;ts=!(Htwu#$PgDkt1?cT~+mm2xWqrcwSA!faME$ZM@8E z#TnS?u1aj?>MB?hY34i#7-a-D{jmp>--L1Jp5BVAKU&|fNu^5Bo_{=s6JNBM+&5Ls z(Uobx`NxSz?WbCD3VS8|JXf>uP7pxvT=ucyxI)g(2^;*@+KFhA#6Y7f=riwQ@_

Zr}pFRaP?bU3kjY$);Q88Jt^2czA!Gx*fvC($j-OT_n zMhwJV>Thy3JDb9{KEs}N54@ZG!#fcj zEiToJRf=vD9?M2ZBVo3IvW!H}AWva|Le4^Z4REnl5ai?Iv%UCF!!?}46| zM=kQbO@V0mg{Yehe-qjPcR42&jvQ2j)wNpM!@7Omp@UVc#)u9NePK~VurpoA$;je# zro}@VkWV3(APS!T!oy1-^Z_jq5S)xmb)R@W8w=6m9xVj*8t*?-IQ&@ zT)G5mVyNFY4vrn{{2R2+*6f?8woVIwQ|^NOvhg&KJ3F;s{-pVdhr}tJ(vao9`+}e? zlxWYgnwrGBx%Vv-!A zEX?=+aL0G`qVRbVTE$X-iEaGm2P1r0 z!s=i&^3W?=hnrAFu-#gt!>}5$xv?3`VE6NBF_3x&UZFn#pRK>Q!&CbQ>+B4gPP&nx z$^f7%SwgHXc>neK2@elX7?~1ECz5IeR&TU>?y}`uEj6t-P=vWqJf>Sw2Yq6I5ofO){zS9x*_Ab!gLh#5M%O`0k@9EB-$Haj~Z;;_)0LE1_Hp>`^bf`5n>Px2sVQg}CC@gM%a2M99-~Rs zZ)pigX>)1#1PUoVY2OcXns<`oGesD^NF+G!i#k$HlV{Z*SjsO#8F& z!=|W{6N~rCOhrPUL!P`fxOctlVNRx@#0)499sXU-gLh`=bAd#b1Z?j8r9?3brP|)1V2jK*>2s5&b49weQ=YX?am3 zD|F%*D{Bvb{M0@pwxm=#E*i1fg}-Z`1ieR_^63_8MKp88=d(%ZdL&3c#C7TS>YJGW zSYoz((X{uM`PQhXzFJallLP>1?$DHUlO>&@wQ zM}Ob|0@OQUNFVQ-ZEL|TC0pM$CThDxs{ZBJdHZqA6Taj{?`x}S(*@FA_vqe@Ut7fm zSx&-9suYB;GTQ7V&3KpyK%@i1PF!y&uCtUYW0dFvZZivlRS!iGbCC(E~xQNr%32)P^WgxpWQ8SnnXfoBLDok-MwU9k34;5AeuSi z4hv`g;{2trTq7%`)oHn5WAqy*rn6kkbr`uXatZvhL^)w}LM5u}esXZ|yU_GctJc=$RqUZ4 zG?MK`VKB!xBFovV*BYePhrre}4{!%{pG-Ux!x(|{=C9YBX1`0q*-_M96z1lT$Rpef zuegRPweRsEsv)wV#x_W!t#0>1Utyl}|6z_kfvgSPy?v}k^f@pxto1495+E8G{ zXrYCmhfT^Zu&#pD_GgO?m%o?C5t+pFL|B}-xH!Twky{g$i?y_beI+~txL0Y0L^?4> z+^Nkte&Mk*C9yJQu&W$q@i_NytB1K{Qg71T7`aGsdwYwp&l*J>?aKlHBSFv3?v8#x z9Mbyz`(Qv&53s?pP=Mk&hu4f2UMs~YzicPT>`{xlnM*rW8OO%O8E0o_Hx#mzQAq^1 zhb*SQU35kGzpEcP<_R@4DEn>yeLL9``z)CeU{4zTzUfXC&Rpw{!ci}zOfQyZ1{rxV zQW)%9CYy?IN+UsyXPer z<`(wXu~STb2cOSmH`b?qFnh}x zO?Ef`3t>qKi9bDo_b!+u5K2pTGB|&FUO~aD>e3-=BA?V{OD!*$6ny~RtAaLlgUg>k zOla?M^CtZwmHX5CZ)h+6$@84OU4J5aj9;F%>76WHWV4jvD4jKx>^x8)jk~;`lKwZ2 zS7TnIicIgfdAqnBO!`Mi#`Vv`{&3l_M#1?@Ah3LNV4btabT#X|ST>coOT>cq^R%?I z;=cC{>&yQw$7pdo%_Y^e5p~?{CVTtLM?xy?Ls*4FUKm$ku?eD zO47E932+`i+2G6k#Y%b?hGGpnYd{#>7M47NlL9SNIJRuWNt{>ZlRX{ajsk{ zB{jA2c~atBFqAWw(+~;E97p|PldT~B=;$nU8;oSx;4K zT^vwatd`KiRvL9q6J&pXuh|+jVPwOX`4sC{%1i*FsrdObb7kp2YE09zdNxeQsmDm< zOupdkNJWOJj+gvEhjPsvA0;%iso~dekhr@GI_Pgp ze?`WbjL3o5mrdL@ms(dRKaknY_}nMdk>U7%8}&X*nMD$WJv>rOPfv+^E;$@Zd@AJ& z-D|UJS_#TiefyE;=t@KAJ>MZ3jN2gb3FNt0sHbOR3y8w7ZksOTsnP;qN4*X(8w;@r zK4hoHF6XqffyKntqFVGj-MD=Wvz# z`30a{J?=Bb3nFI0&Pt9dDrUVQXK}p*(lU8{(Y?w>6gW{o^SJkE_Wy`#Mf_5pKG2sf znPqY!9V^uS#ksvrjQ72i*%<=6jRK}r3B&47-ZR4#y~C3Z=V!EQ+8;uEle9#}_V2Na z;^etHaf-=`#0Xhn76Bx(bAPK&XIhr3gafo}hiO!fDI3i7U%YDV#jSV4T_o~S@3hyU&ocs3_6@gXRsnhe8RAam>f;z9()w51YZsLpLz$;Cc z7|^kAAc&ulASKYAWcW1gK(9P}yJlrw)tXG$YBm<`rkI%bWbeAFX!%8`*p4}hZ-bDC zk0XUWok-l!@>Q-g`6+tv2Y4Q{rMs)_^rlz18+lUyqak z%EP0#@83JbXO|wDi4oCPsz_SMGk=7Hg(shFMiSXJA`7-J{U~~1=ocu&xRJCtk&jMt z78W^+#~tUY-pyz&#X%WkuRZ16SmdIq^3?S>x3++vlC=kGwM5Re@kKWjZ38r(DT>}_LdsdIp%V;nV(V%f-c(VbmV3|xvF z8Ql^zBK42}pzBXT-e{%b8D55cFYlA-X-|!I87aRJ6 z5LZPD28w}SYTmcrUuav|hK{evdQZJ}Xib>w)-iOORW1*vJZ&e8UH}7)3rh4gV1VWh5;AtLlKJ1XH~w!mH7{`@w_+v}6EA6N@6#YwSd~a-)0^g^ zL1|;Q5}pkWG(W#bFagyz=!C66!i06kvgza(+Y+?{d~{?!p*wZzMrB5wViWFiW3J@K z40Q&B%B`(-0NPu{m}^vJ;Ne$A;wA!&od9B=ykNy^t$lcd_>^$RT0W;HZ6N;jU^)75 z5HMCE382$hP>Zm!uzm$D1!l%DS)k{Ez@hfQQt?GX3}C9CQu>Da$l z?k8MLyR^!V0#R(yf&>^aIe{l%uS%?C{K=tzS32$%zM1W-iUVgjfYUjBW=J)OBbm8IQg_V^x zDowo<`8n!GYY!Sda@*i|w$T0*^e0}=kq>OL0&92|D@%icvWrt%tDaFx2oWjdxKQZ+ zfQm*GioCU#<7%7Z-11wfx%pn~T_b9hfp6?9t}6sQE_?nMZiU~^3~Ge1yImqA zO~W~!D}NKSfuFC&cuFo2d_W_fe6t8jOQio{QN!8c3qu1+jGw4R@$?T%$%yg|!d(AJ z&%=KZVi~cA5wZ~HV^OCYWb$ee6-FJ@_>b^kn53*`9k0lO5+O|?qtO& zzkK*(cEXcEj^s)3XnpkfRl-KxE^54Y_`z`0-wIJqrcb*WH1=!3ClEAq0=c$V$|Zc| z_4PK<6E0)nA0xc-Lu(7?4s_!qE(_@|2K+LSt1;t$XlqotfeNe!(^j-yk@z*L!vE%& zeb@aGIXjlFAY zi%@R+m=x{}p<+gYkQDcwt@nJk=g-n-tu$Dkn`816|H}v+o4g5pvM%O-&rZ@Cy#n{? zAolFY8`M4Q5@Dq3+rE?q+C=vDhK9UYs|i?q_cA1mRS5QRqX+=zRiP!bOa8r!5vlJ$ tf`ztHT+B$O6ASwPGF$&2TXq0KhQ%1seq>M10O-4s)ReRoYvj$t{|6^k?7aX0 diff --git a/packages/schema/bin/cli b/packages/schema/bin/cli deleted file mode 100755 index 1bd15e4f6..000000000 --- a/packages/schema/bin/cli +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node - -require('../cli').default(); diff --git a/packages/schema/bin/post-install.js b/packages/schema/bin/post-install.js deleted file mode 100644 index f788bc1d7..000000000 --- a/packages/schema/bin/post-install.js +++ /dev/null @@ -1,31 +0,0 @@ -try { - if (process.env.DO_NOT_TRACK == '1') { - process.exit(0); - } - - const Mixpanel = require('mixpanel'); - const machineId = require('node-machine-id'); - const os = require('os'); - const isDocker = require('../utils/is-docker').default; - - const mixpanel = Mixpanel.init('', { - geolocate: true, - }); - - const version = require('../package.json').version; - const payload = { - distinct_id: machineId.machineIdSync(), - nodeVersion: process.version, - time: new Date(), - $os: os.type(), - osType: os.type(), - osRelease: os.release(), - osPlatform: os.platform(), - osArch: os.arch(), - osVersion: os.version(), - isDocker: isDocker(), - version, - }; - - mixpanel.track('npm:install', payload); -} catch {} diff --git a/packages/schema/build/bundle.js b/packages/schema/build/bundle.js deleted file mode 100644 index 1cd8d47da..000000000 --- a/packages/schema/build/bundle.js +++ /dev/null @@ -1,44 +0,0 @@ -const watch = process.argv.includes('--watch'); -const minify = process.argv.includes('--minify'); -const success = watch ? 'Watch build succeeded' : 'Build succeeded'; -const fs = require('fs'); -require('dotenv').config({ path: './.env.local' }); -require('dotenv').config({ path: './.env' }); - -// Replace telemetry token in generated bundle files after building -function replaceTelemetryTokenInBundle() { - const telemetryToken = process.env.VSCODE_TELEMETRY_TRACKING_TOKEN; - if (!telemetryToken) { - console.error('Error: VSCODE_TELEMETRY_TRACKING_TOKEN environment variable is not set'); - process.exit(1); - } - const file = 'bundle/extension.js'; - let content = fs.readFileSync(file, 'utf-8'); - content = content.replace('', telemetryToken); - fs.writeFileSync(file, content, 'utf-8'); -} - -require('esbuild') - .build({ - entryPoints: ['src/extension.ts', 'src/language-server/main.ts'], - outdir: 'bundle', - bundle: true, - external: ['vscode', '@prisma/*'], - platform: 'node', - sourcemap: !minify, - minify, - }) - .then(() => { - // Replace the token after building outputs - replaceTelemetryTokenInBundle(); - }) - .then(() => { - fs.cpSync('./src/res', 'bundle/res', { force: true, recursive: true }); - fs.cpSync('./src/vscode/res', 'bundle/res', { force: true, recursive: true }); - fs.cpSync('../language/syntaxes', 'bundle/syntaxes', { force: true, recursive: true }); - }) - .then(() => console.log(success)) - .catch((err) => { - console.error(err); - process.exit(1); - }); diff --git a/packages/schema/build/post-build.js b/packages/schema/build/post-build.js deleted file mode 100644 index c8623f4c5..000000000 --- a/packages/schema/build/post-build.js +++ /dev/null @@ -1,29 +0,0 @@ -require('dotenv').config({ path: './.env.local' }); -require('dotenv').config({ path: './.env' }); -const fs = require('fs'); - -const filesToReplace = ['dist/bin/post-install.js', 'dist/constants.js']; -for (const file of filesToReplace) { - let content = fs.readFileSync(file, 'utf-8'); - if (process.env.TELEMETRY_TRACKING_TOKEN) { - content = content.replace('', process.env.TELEMETRY_TRACKING_TOKEN); - } else { - content = content.replace('', ''); - } - console.log('Updating file:', file); - fs.writeFileSync(file, content, { - encoding: 'utf-8', - }); -} - -let cliContent = fs.readFileSync('dist/cli/index.js', 'utf-8'); -if (process.env.DEFAULT_NPM_TAG) { - cliContent = cliContent.replace('', process.env.DEFAULT_NPM_TAG); -} else { - cliContent = cliContent.replace('', 'latest'); -} - -console.log('Updating file: dist/cli/index.js'); -fs.writeFileSync('dist/cli/index.js', cliContent, { - encoding: 'utf-8', -}); diff --git a/packages/schema/jest.config.ts b/packages/schema/jest.config.ts deleted file mode 120000 index a12d812fd..000000000 --- a/packages/schema/jest.config.ts +++ /dev/null @@ -1 +0,0 @@ -../../jest.config.ts \ No newline at end of file diff --git a/packages/schema/language-configuration.json b/packages/schema/language-configuration.json deleted file mode 100644 index 4000da2d9..000000000 --- a/packages/schema/language-configuration.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "comments": { - // symbol used for single line comment. Remove this entry if your language does not support line comments - "lineComment": "//", - // symbols used for start and end a block comment. Remove this entry if your language does not support block comments - "blockComment": ["/*", "*/"] - }, - // symbols used as brackets - "brackets": [ - ["{", "}"], - ["[", "]"], - ["(", ")"] - ], - // symbols that are auto closed when typing - "autoClosingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"], - { "open": "/**", "close": " */", "notIn": ["string"] } - ], - // symbols that can be used to surround a selection - "surroundingPairs": [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ["\"", "\""], - ["'", "'"] - ], - "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\#\\%\\^\\&\\*\\-\\=\\+\\{\\}\\(\\)\\[\\]\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\s]+)" -} diff --git a/packages/schema/src/cli/actions/check.ts b/packages/schema/src/cli/actions/check.ts deleted file mode 100644 index df161b5c0..000000000 --- a/packages/schema/src/cli/actions/check.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getDefaultSchemaLocation, loadDocument } from '../cli-util'; - -type Options = { - schema: string; -}; - -/** - * CLI action for checking schema - */ -export async function check(_projectPath: string, options: Options) { - const schema = options.schema ?? getDefaultSchemaLocation(); - await loadDocument(schema); - console.log('The schema is valid.'); -} diff --git a/packages/schema/src/cli/actions/format.ts b/packages/schema/src/cli/actions/format.ts deleted file mode 100644 index c3d2a6bed..000000000 --- a/packages/schema/src/cli/actions/format.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getVersion } from '@zenstackhq/runtime'; -import colors from 'colors'; -import fs from 'fs'; -import { writeFile } from 'fs/promises'; -import ora from 'ora'; -import { CliError } from '../cli-error'; -import { formatDocument, getDefaultSchemaLocation } from '../cli-util'; - -type Options = { - schema: string; - prismaStyle?: boolean; -}; - -export async function format(_projectPath: string, options: Options) { - const version = getVersion(); - console.log(colors.bold(`⌛️ ZenStack CLI v${version}`)); - - const schemaFile = options.schema ?? getDefaultSchemaLocation(); - if (!fs.existsSync(schemaFile)) { - console.error(colors.red(`File ${schemaFile} does not exist.`)); - throw new CliError('schema file does not exist'); - } - - const spinner = ora(`Formatting ${schemaFile}`).start(); - try { - const formattedDoc = await formatDocument(schemaFile, options.prismaStyle); - await writeFile(schemaFile, formattedDoc); - spinner.succeed(); - } catch (e) { - spinner.fail(); - throw e; - } -} diff --git a/packages/schema/src/cli/actions/generate.ts b/packages/schema/src/cli/actions/generate.ts deleted file mode 100644 index 022c43e12..000000000 --- a/packages/schema/src/cli/actions/generate.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { getPrismaClientGenerator, PluginError } from '@zenstackhq/sdk'; -import { isPlugin } from '@zenstackhq/sdk/ast'; -import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; -import colors from 'colors'; -import path from 'path'; -import semver from 'semver'; -import { CliError } from '../cli-error'; -import { - checkNewVersion, - checkRequiredPackage, - getDefaultSchemaLocation, - getZenStackPackages, - loadDocument, - requiredPrismaVersion, - showNotification, -} from '../cli-util'; -import { PluginRunner, PluginRunnerOptions } from '../plugin-runner'; - -type Options = { - schema?: string; - output?: string; - dependencyCheck: boolean; - versionCheck: boolean; - compile: boolean; - withPlugins?: string[]; - withoutPlugins?: string[]; - defaultPlugins: boolean; - offline?: boolean; -}; - -/** - * CLI action for generating code from schema - */ -export async function generate(projectPath: string, options: Options) { - if (options.dependencyCheck) { - checkRequiredPackage('prisma', requiredPrismaVersion); - checkRequiredPackage('@prisma/client', requiredPrismaVersion); - } - - // check for multiple versions of Zenstack packages - const packages = getZenStackPackages(projectPath); - if (packages.length > 0) { - const versions = new Set(packages.map((p) => p.version).filter((v): v is string => !!v)); - if (versions.size > 1) { - console.warn( - colors.yellow( - 'WARNING: Multiple versions of Zenstack packages detected. Run "zenstack info" to see details.' - ) - ); - } - } - - const prismaVersion = getPrismaVersion(); - if (prismaVersion && semver.gte(prismaVersion, '7.0.0')) { - console.warn(colors.yellow('Prisma 7 support is untested and not planned. Use with caution.')); - } - - await runPlugins(options); - - // note that we can't run online jobs concurrently with plugins because - // plugins are CPU-bound and can cause false timeout - const postJobs: Promise[] = []; - - if (options.versionCheck && !options.offline) { - postJobs.push(checkNewVersion()); - } - - if (!options.offline) { - postJobs.push(showNotification()); - } - - await Promise.all(postJobs); -} - -async function runPlugins(options: Options) { - const schema = options.schema ?? getDefaultSchemaLocation(); - - const model = await loadDocument(schema); - - const gen = getPrismaClientGenerator(model); - if (gen?.isNewGenerator && !options.output) { - console.error( - colors.red( - 'When using the "prisma-client" generator, you must provide an explicit output path with the "--output" CLI parameter.' - ) - ); - throw new CliError( - 'When using with the "prisma-client" generator, you must provide an explicit output path with the "--output" CLI parameter.' - ); - } - - for (const name of [...(options.withPlugins ?? []), ...(options.withoutPlugins ?? [])]) { - const pluginDecl = model.declarations.find((d) => isPlugin(d) && d.name === name); - if (!pluginDecl) { - console.error(colors.red(`Plugin "${name}" not found in schema.`)); - throw new CliError(`Plugin "${name}" not found in schema.`); - } - } - - const runnerOpts: PluginRunnerOptions = { - schema: model, - schemaPath: path.resolve(schema), - withPlugins: options.withPlugins, - withoutPlugins: options.withoutPlugins, - defaultPlugins: options.defaultPlugins, - output: options.output, - compile: options.compile, - }; - - try { - await new PluginRunner().run(runnerOpts); - } catch (err) { - if (err instanceof PluginError) { - console.error(colors.red(`${err.plugin}: ${err.message}`)); - throw new CliError(err.message); - } else { - throw err; - } - } -} diff --git a/packages/schema/src/cli/actions/index.ts b/packages/schema/src/cli/actions/index.ts deleted file mode 100644 index 7e172d4bc..000000000 --- a/packages/schema/src/cli/actions/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './check'; -export * from './format'; -export * from './generate'; -export * from './info'; -export * from './init'; -export * from './repl'; diff --git a/packages/schema/src/cli/actions/info.ts b/packages/schema/src/cli/actions/info.ts deleted file mode 100644 index c212babf4..000000000 --- a/packages/schema/src/cli/actions/info.ts +++ /dev/null @@ -1,50 +0,0 @@ -import colors from 'colors'; -import ora from 'ora'; -import semver from 'semver'; -import { getLatestVersion, getZenStackPackages } from '../cli-util'; - -/** - * CLI action for getting information about installed ZenStack packages - */ -export async function info(projectPath: string) { - const packages = getZenStackPackages(projectPath); - if (!packages) { - console.error('Unable to locate package.json. Are you in a valid project directory?'); - return; - } - - console.log('Installed ZenStack Packages:'); - const versions = new Set(); - for (const { pkg, version } of packages) { - if (version) { - versions.add(version); - } - console.log(` ${colors.green(pkg.padEnd(20))}\t${version}`); - } - - if (versions.size > 1) { - console.warn(colors.yellow('WARNING: Multiple versions of Zenstack packages detected. This may cause issues.')); - } else if (versions.size > 0) { - const spinner = ora('Checking npm registry').start(); - - let latest: string; - - try { - latest = await getLatestVersion(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - spinner.fail(`Failed to get latest version: ${err.message}`); - return; - } - - spinner.succeed(); - const version = [...versions][0]; - if (semver.gt(latest, version)) { - console.log(`A newer version of Zenstack is available: ${latest}.`); - } else if (semver.gt(version, latest)) { - console.log('You are using a pre-release version of Zenstack.'); - } else { - console.log('You are using the latest version of Zenstack.'); - } - } -} diff --git a/packages/schema/src/cli/actions/init.ts b/packages/schema/src/cli/actions/init.ts deleted file mode 100644 index 2d77c93e1..000000000 --- a/packages/schema/src/cli/actions/init.ts +++ /dev/null @@ -1,83 +0,0 @@ -import colors from 'colors'; -import fs from 'fs'; -import path from 'path'; -import { PackageManagers, ensurePackage, installPackage } from '../../utils/pkg-utils'; -import { getVersion } from '../../utils/version-utils'; -import { CliError } from '../cli-error'; -import { checkNewVersion } from '../cli-util'; - -type Options = { - prisma: string | undefined; - packageManager: PackageManagers | undefined; - versionCheck: boolean; - tag?: string; -}; - -/** - * CLI action for initializing an existing project - */ -export async function init(projectPath: string, options: Options) { - if (!fs.existsSync(projectPath)) { - console.error(`Path does not exist: ${projectPath}`); - throw new CliError('project path does not exist'); - } - - const defaultPrismaSchemaLocation = './prisma/schema.prisma'; - let prismaSchema = options.prisma; - if (prismaSchema) { - if (!fs.existsSync(prismaSchema)) { - console.error(`Prisma schema file does not exist: ${prismaSchema}`); - throw new CliError('prisma schema does not exist'); - } - } else if (fs.existsSync(defaultPrismaSchemaLocation)) { - prismaSchema = defaultPrismaSchemaLocation; - } - - const zmodelFile = path.join(projectPath, './schema.zmodel'); - let sampleModelGenerated = false; - - if (fs.existsSync(zmodelFile)) { - console.warn(`ZenStack model already exists at ${zmodelFile}, not generating a new one.`); - } else { - if (prismaSchema) { - // copy over schema.prisma - fs.copyFileSync(prismaSchema, zmodelFile); - } else { - // create a new model - const starterContent = fs.readFileSync(path.join(__dirname, '../../res/starter.txt'), 'utf-8'); - fs.writeFileSync(zmodelFile, starterContent); - sampleModelGenerated = true; - } - } - - const latestSupportedPrismaVersion = getLatestSupportedPrismaVersion(); - - ensurePackage('prisma', true, options.packageManager, latestSupportedPrismaVersion, projectPath); - ensurePackage('@prisma/client', false, options.packageManager, latestSupportedPrismaVersion, projectPath); - - const tag = options.tag ?? getVersion(); - installPackage('zenstack', true, options.packageManager, tag, projectPath); - installPackage('@zenstackhq/runtime', false, options.packageManager, tag, projectPath); - - if (sampleModelGenerated) { - console.log(`Sample model generated at: ${colors.blue(zmodelFile)} - -Learn how to use ZenStack: https://zenstack.dev/docs.`); - } else if (prismaSchema) { - console.log( - `Your current Prisma schema "${prismaSchema}" has been copied to "${zmodelFile}". -Moving forward please edit this file and run "zenstack generate" to regenerate Prisma schema.` - ); - } - - console.log(colors.green('\nProject initialized successfully!')); - - if (options.versionCheck) { - await checkNewVersion(); - } -} - -function getLatestSupportedPrismaVersion() { - // Prisma 7 is not tested - return '6'; -} diff --git a/packages/schema/src/cli/actions/repl.ts b/packages/schema/src/cli/actions/repl.ts deleted file mode 100644 index fa291a3e2..000000000 --- a/packages/schema/src/cli/actions/repl.ts +++ /dev/null @@ -1,218 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-var-requires */ -import colors from 'colors'; -import path from 'path'; -import { inspect } from 'util'; - -// inspired by: https://github.com/Kinjalrk2k/prisma-console - -/** - * CLI action for starting a REPL session - */ -export async function repl( - projectPath: string, - options: { loadPath?: string; prismaClient?: string; debug?: boolean; table?: boolean } -) { - if (!process?.stdout?.isTTY && process?.versions?.bun) { - console.error( - 'REPL on Bun is only available in a TTY terminal at this time. Please use npm/npx to run the command in this context instead of bun/bunx.' - ); - return; - } - - const prettyRepl = await import('pretty-repl'); - - console.log('Welcome to ZenStack REPL. See help with the ".help" command.'); - console.log('Global variables:'); - console.log(` ${colors.blue('db')} to access enhanced PrismaClient`); - console.log(` ${colors.blue('prisma')} to access raw PrismaClient`); - console.log(` ${colors.blue('user')} to inspect the current user`); - console.log('Commands:'); - console.log(` ${colors.magenta('.auth { id: ... }')} - set current user`); - console.log(` ${colors.magenta('.table')} - toggle table output`); - console.log(` ${colors.magenta('.debug')} - toggle debug output`); - console.log(); - console.log(`Running as anonymous user. Use ".auth" to set current user.`); - - let PrismaClient: any; - - const prismaClientModule = options.prismaClient ?? '@prisma/client'; - - try { - // try direct require - const module = require(prismaClientModule); - PrismaClient = module.PrismaClient; - } catch (err) { - if (!path.isAbsolute(prismaClientModule)) { - // try relative require - const module = require(path.join(projectPath, prismaClientModule)); - PrismaClient = module.PrismaClient; - } else { - throw err; - } - } - - const { enhance } = options.loadPath - ? require(path.join(path.resolve(options.loadPath), 'enhance')) - : require('@zenstackhq/runtime'); - - let debug = !!options.debug; - let table = !!options.table; - let prisma: any; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let db: any; - let user: any; - - const replServer = prettyRepl.start({ - prompt: `[${colors.cyan('anonymous')}] > `, - eval: async (cmd, _context, _filename, callback) => { - try { - let r: any = undefined; - let isPrismaCall = false; - - if (/^\s*user\s*=[^=]/.test(cmd)) { - // assigning to user variable, reset auth - eval(cmd); - setAuth(user); - } else if (/^\s*await\s+/.test(cmd)) { - // eval can't handle top-level await, so we wrap it in an async function - cmd = `(async () => (${cmd}))()`; - r = eval(cmd); - if (isPrismaPromise(r)) { - isPrismaCall = true; - } - r = await r; - } else { - r = eval(cmd); - if (isPrismaPromise(r)) { - isPrismaCall = true; - // automatically await Prisma promises - r = await r; - } - } - - if (isPrismaCall && table) { - console.table(r); - callback(null, undefined); - } else { - callback(null, r); - } - } catch (err: any) { - if (err.code) { - console.error(colors.red(err.message)); - console.error('Code:', err.code); - if (err.meta) { - console.error('Meta:', err.meta); - } - callback(null, undefined); - } else { - callback(err as Error, undefined); - } - } - }, - }); - - // .table command - replServer.defineCommand('table', { - help: 'Toggle table output', - action(value: string) { - if (value && value !== 'on' && value !== 'off' && value !== 'true' && value !== 'false') { - console.error('Invalid argument. Usage: .table [on|off|true|false]'); - this.displayPrompt(); - return; - } - this.clearBufferedCommand(); - table = value ? value === 'on' || value === 'true' : !table; - console.log('Table output:', table); - this.displayPrompt(); - }, - }); - - // .debug command - replServer.defineCommand('debug', { - help: 'Toggle debug output', - async action(value: string) { - if (value && value !== 'on' && value !== 'off' && value !== 'true' && value !== 'false') { - console.error('Invalid argument. Usage: .debug [on|off|true|false]'); - this.displayPrompt(); - return; - } - this.clearBufferedCommand(); - debug = value ? value === 'on' || value === 'true' : !debug; - console.log('Debug mode:', debug); - await createClient(); - setPrompt(); - this.displayPrompt(); - }, - }); - - // .auth command - replServer.defineCommand('auth', { - help: 'Set current user. Run without argument to switch to anonymous. Pass an user object to set current user. Run ".auth info" to show current user.', - action(value: string) { - this.clearBufferedCommand(); - try { - if (!value?.trim()) { - // set anonymous - setAuth(undefined); - console.log(`Auth user: anonymous. Use ".auth { id: ... }" to change.`); - } else if (value.trim() === 'info') { - // refresh auth user - setAuth(user); - console.log(`Current user: ${user ? inspect(user) : 'anonymous'}`); - } else { - // set current user - const user = eval(`(${value})`); - if (!user || typeof user !== 'object') { - console.error(`Invalid argument. Pass a user object like { id: ... }`); - this.displayPrompt(); - return; - } - setAuth(user); - console.log(`Auth user: ${inspect(user)}. Use ".auth" to switch to anonymous.`); - } - } catch (err: any) { - console.error('Unable to set auth user:', err.message); - } - this.displayPrompt(); - }, - }); - - replServer.setupHistory(path.join(projectPath, './.zenstack_repl_history'), (err) => { - if (err) { - console.error('unable to setup REPL history:', err); - } - }); - - setPrompt(); - await createClient(); - - async function createClient() { - if (prisma) { - prisma.$disconnect(); - } - prisma = new PrismaClient(debug ? { log: ['info'] } : undefined); - // https://github.com/prisma/prisma/issues/18292 - prisma[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient'; - db = enhance(prisma, { user }, { logPrismaQuery: debug }); - - replServer.context.prisma = prisma; - replServer.context.db = db; - } - - function setPrompt() { - const userInfo = user ? (user.id ? `user#${user.id.toString().slice(-8)}` : inspect(user)) : 'anonymous'; - replServer.setPrompt(`[${debug ? colors.yellow('D ') : ''}${colors.cyan(userInfo)}] > `); - } - - function setAuth(_user: unknown) { - user = _user; - // recreate enhanced PrismaClient - db = replServer.context.db = enhance(prisma, { user }, { logPrismaQuery: debug }); - setPrompt(); - } -} - -function isPrismaPromise(r: any) { - return r?.[Symbol.toStringTag] === 'PrismaPromise' || r?.[Symbol.toStringTag] === 'ZenStackPromise'; -} diff --git a/packages/schema/src/cli/cli-error.ts b/packages/schema/src/cli/cli-error.ts deleted file mode 100644 index bf65c26a4..000000000 --- a/packages/schema/src/cli/cli-error.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Indicating an error during CLI execution - */ -export class CliError extends Error {} diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts deleted file mode 100644 index 54ac123bd..000000000 --- a/packages/schema/src/cli/cli-util.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { isDataModel, isDataSource, isPlugin, Model } from '@zenstackhq/language/ast'; -import { getDataModelAndTypeDefs, getLiteral, hasAttribute } from '@zenstackhq/sdk'; -import colors from 'colors'; -import fs from 'fs'; -import { getDocument, LangiumDocument, LangiumDocuments, linkContentToContainer } from 'langium'; -import { NodeFileSystem } from 'langium/node'; -import path from 'path'; -import semver from 'semver'; -import terminalLink from 'terminal-link'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { URI } from 'vscode-uri'; -import { z } from 'zod'; -import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../language-server/constants'; -import { ZModelFormatter } from '../language-server/zmodel-formatter'; -import { createZModelServices, ZModelServices } from '../language-server/zmodel-module'; -import { mergeBaseModels, resolveImport, resolveTransitiveImports } from '../utils/ast-utils'; -import { findUp } from '../utils/pkg-utils'; -import { getVersion } from '../utils/version-utils'; -import { CliError } from './cli-error'; - -// required minimal version of Prisma -export const requiredPrismaVersion = '4.8.0'; - -const CHECK_VERSION_TIMEOUT = 1000; -const FETCH_CLI_CONFIG_TIMEOUT = 500; -const CLI_CONFIG_ENDPOINT = 'https://zenstack.dev/config/cli.json'; - -/** - * Loads a zmodel document from a file. - * @param fileName File name - * @param services Language services - * @returns Parsed and validated AST - */ -export async function loadDocument(fileName: string, validateOnly = false): Promise { - const services = createZModelServices(NodeFileSystem).ZModel; - const extensions = services.LanguageMetaData.fileExtensions; - if (!extensions.includes(path.extname(fileName))) { - console.error(colors.yellow(`Please choose a file with extension: ${extensions}.`)); - throw new CliError('invalid schema file'); - } - - if (!fs.existsSync(fileName)) { - console.error(colors.red(`File ${fileName} does not exist.`)); - throw new CliError('schema file does not exist'); - } - - // load standard library - const stdLib = services.shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file(path.resolve(path.join(__dirname, '../res', STD_LIB_MODULE_NAME))) - ); - - // load documents provided by plugins - const pluginDocuments = await getPluginDocuments(services, fileName); - - const langiumDocuments = services.shared.workspace.LangiumDocuments; - // load the document - const document = langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName))); - - // load all imports - const importedURIs = eagerLoadAllImports(document, langiumDocuments); - - const importedDocuments = importedURIs.map((uri) => langiumDocuments.getOrCreateDocument(uri)); - - // build the document together with standard library, plugin modules, and imported documents - await services.shared.workspace.DocumentBuilder.build( - [stdLib, ...pluginDocuments, document, ...importedDocuments], - { - validationChecks: 'all', - } - ); - - const diagnostics = langiumDocuments.all - .flatMap((doc) => (doc.diagnostics ?? []).map((diag) => ({ doc, diag }))) - .filter(({ diag }) => diag.severity === 1 || diag.severity === 2) - .toArray(); - - let hasErrors = false; - - if (diagnostics.length > 0) { - for (const { doc, diag } of diagnostics) { - const message = `${path.relative(process.cwd(), doc.uri.fsPath)}:${diag.range.start.line + 1}:${ - diag.range.start.character + 1 - } - ${diag.message}`; - - if (diag.severity === 1) { - console.error(colors.red(message)); - hasErrors = true; - } else { - console.warn(colors.yellow(message)); - } - } - - if (hasErrors) { - throw new CliError('Schema contains validation errors'); - } - } - - const model = document.parseResult.value as Model; - - if (validateOnly) { - return model; - } - - // merge all declarations into the main document - const imported = mergeImportsDeclarations(langiumDocuments, model); - - // remove imported documents - imported.forEach((model) => langiumDocuments.deleteDocument(model.$document!.uri)); - services.shared.workspace.IndexManager.remove(imported.map((model) => model.$document!.uri)); - - // extra validation after merging imported declarations - validationAfterImportMerge(model); - - // merge fields and attributes from base models - mergeBaseModels(model, services.references.Linker); - - // finally relink all references - const relinkedModel = await relinkAll(model, services); - - // filter out data model fields marked with `@ignore` - filterIgnoredFields(relinkedModel); - - return relinkedModel; -} - -// check global unique thing after merge imports -function validationAfterImportMerge(model: Model) { - const dataSources = model.declarations.filter((d) => isDataSource(d)); - if (dataSources.length == 0) { - console.error(colors.red('Validation error: Model must define a datasource')); - throw new CliError('schema validation errors'); - } else if (dataSources.length > 1) { - console.error(colors.red('Validation error: Multiple datasource declarations are not allowed')); - throw new CliError('schema validation errors'); - } - - // at most one `@@auth` model - const decls = getDataModelAndTypeDefs(model, true); - const authDecls = decls.filter((d) => hasAttribute(d, '@@auth')); - if (authDecls.length > 1) { - console.error(colors.red('Validation error: Multiple `@@auth` declarations are not allowed')); - throw new CliError('schema validation errors'); - } -} - -export function eagerLoadAllImports( - document: LangiumDocument, - documents: LangiumDocuments, - uris: Set = new Set() -) { - const uriString = document.uri.toString(); - if (!uris.has(uriString)) { - uris.add(uriString); - const model = document.parseResult.value as Model; - - for (const imp of model.imports) { - const importedModel = resolveImport(documents, imp); - if (importedModel) { - const importedDoc = getDocument(importedModel); - eagerLoadAllImports(importedDoc, documents, uris); - } - } - } - - return Array.from(uris) - .filter((x) => uriString != x) - .map((e) => URI.parse(e)); -} - -export function mergeImportsDeclarations(documents: LangiumDocuments, model: Model) { - const importedModels = resolveTransitiveImports(documents, model); - - const importedDeclarations = importedModels.flatMap((m) => m.declarations); - model.declarations.push(...importedDeclarations); - - // remove import directives - model.imports = []; - - // fix $containerIndex - linkContentToContainer(model); - - return importedModels; -} - -export async function getPluginDocuments(services: ZModelServices, fileName: string): Promise { - // parse the user document (without validation) - const parseResult = services.parser.LangiumParser.parse(fs.readFileSync(fileName, { encoding: 'utf-8' })); - const parsed = parseResult.value as Model; - - // traverse plugins and collect "plugin.zmodel" documents - const result: LangiumDocument[] = []; - parsed.declarations.forEach((decl) => { - if (isPlugin(decl)) { - const providerField = decl.fields.find((f) => f.name === 'provider'); - if (providerField) { - const provider = getLiteral(providerField.value); - if (provider) { - let pluginEntrance: string | undefined; - try { - // direct require - pluginEntrance = require.resolve(provider); - } catch { - if (!path.isAbsolute(provider)) { - // relative path - try { - pluginEntrance = require.resolve(path.join(path.dirname(fileName), provider)); - } catch { - // noop - } - } - } - - if (pluginEntrance) { - const pluginModelFile = path.join(path.dirname(pluginEntrance), PLUGIN_MODULE_NAME); - if (fs.existsSync(pluginModelFile)) { - result.push( - services.shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file(pluginModelFile) - ) - ); - } - } - } - } - } - }); - return result; -} - -export function getZenStackPackages(projectPath: string): Array<{ pkg: string; version: string | undefined }> { - let pkgJson: { dependencies: Record; devDependencies: Record }; - const resolvedPath = path.resolve(projectPath); - try { - pkgJson = require(path.join(resolvedPath, 'package.json')); - } catch { - return []; - } - - const packages = [ - ...Object.keys(pkgJson.dependencies ?? {}).filter((p) => p.startsWith('@zenstackhq/')), - ...Object.keys(pkgJson.devDependencies ?? {}).filter((p) => p.startsWith('@zenstackhq/')), - ]; - - const result = packages.map((pkg) => { - try { - const resolved = require.resolve(`${pkg}/package.json`, { paths: [resolvedPath] }); - // eslint-disable-next-line @typescript-eslint/no-var-requires - return { pkg, version: require(resolved).version as string }; - } catch { - return { pkg, version: undefined }; - } - }); - - result.splice(0, 0, { pkg: 'zenstack', version: getVersion() }); - - return result; -} - -export function checkRequiredPackage(packageName: string, minVersion?: string) { - let packageVersion: string; - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - packageVersion = require(`${packageName}/package.json`).version; - } catch (error) { - console.error(colors.red(`${packageName} not found, please install it`)); - throw new CliError(`${packageName} not found`); - } - - if (minVersion && semver.lt(packageVersion, minVersion)) { - console.error( - colors.red( - `${packageName} needs to be above ${minVersion}, the installed version is ${packageVersion}, please upgrade it` - ) - ); - throw new CliError(`${packageName} version is too low`); - } -} - -export async function checkNewVersion() { - const currVersion = getVersion(); - let latestVersion: string; - try { - latestVersion = await getLatestVersion(); - } catch { - // noop - return; - } - - if (latestVersion && currVersion && semver.gt(latestVersion, currVersion)) { - console.log(`A newer version ${colors.cyan(latestVersion)} is available.`); - } -} - -export async function getLatestVersion() { - const fetchResult = await fetch('https://registry.npmjs.org/zenstack', { - headers: { accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*' }, - signal: AbortSignal.timeout(CHECK_VERSION_TIMEOUT), - }); - - if (fetchResult.ok) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data: any = await fetchResult.json(); - const latestVersion = data?.['dist-tags']?.latest; - if (typeof latestVersion === 'string' && semver.valid(latestVersion)) { - return latestVersion; - } - } - - throw new Error('invalid npm registry response'); -} - -export async function formatDocument(fileName: string, isPrismaStyle = true) { - const services = createZModelServices(NodeFileSystem).ZModel; - const extensions = services.LanguageMetaData.fileExtensions; - if (!extensions.includes(path.extname(fileName))) { - console.error(colors.yellow(`Please choose a file with extension: ${extensions}.`)); - throw new CliError('invalid schema file'); - } - - const langiumDocuments = services.shared.workspace.LangiumDocuments; - const document = langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName))); - - const formatter = services.lsp.Formatter as ZModelFormatter; - - formatter.setPrismaStyle(isPrismaStyle); - - const identifier = { uri: document.uri.toString() }; - const options = formatter.getFormatOptions() ?? { - insertSpaces: true, - tabSize: 4, - }; - - const edits = await formatter.formatDocument(document, { options, textDocument: identifier }); - return TextDocument.applyEdits(document.textDocument, edits); -} - -export function getDefaultSchemaLocation() { - // handle override from package.json - const pkgJsonPath = findUp(['package.json']); - if (pkgJsonPath) { - const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); - if (typeof pkgJson?.zenstack?.schema === 'string') { - if (path.isAbsolute(pkgJson.zenstack.schema)) { - return pkgJson.zenstack.schema; - } else { - // resolve relative to package.json - return path.resolve(path.dirname(pkgJsonPath), pkgJson.zenstack.schema); - } - } - } - - return path.resolve('schema.zmodel'); -} - -async function relinkAll(model: Model, services: ZModelServices) { - const doc = model.$document!; - - // unlink the document - services.references.Linker.unlink(doc); - - // remove current document - await services.shared.workspace.DocumentBuilder.update([], [doc.uri]); - - // recreate and load the document - const newDoc = services.shared.workspace.LangiumDocumentFactory.fromModel(model, doc.uri); - services.shared.workspace.LangiumDocuments.addDocument(newDoc); - - // rebuild the document - await services.shared.workspace.DocumentBuilder.build([newDoc], { validationChecks: 'all' }); - - return newDoc.parseResult.value as Model; -} - -function filterIgnoredFields(model: Model) { - model.declarations.forEach((decl) => { - if (!isDataModel(decl)) { - return; - } - decl.$allFields = [...decl.fields]; - decl.fields = decl.fields.filter((f) => !hasAttribute(f, '@ignore')); - }); -} - -export async function showNotification() { - try { - const fetchResult = await fetch(CLI_CONFIG_ENDPOINT, { - headers: { accept: 'application/json' }, - signal: AbortSignal.timeout(FETCH_CLI_CONFIG_TIMEOUT), - }); - - if (!fetchResult.ok) { - return; - } - - const data = await fetchResult.json(); - const schema = z.object({ - notifications: z.array(z.object({ title: z.string(), url: z.string().url(), active: z.boolean() })), - }); - const parseResult = schema.safeParse(data); - - if (parseResult.success) { - const activeItems = parseResult.data.notifications.filter((item) => item.active); - // return a random active item - if (activeItems.length > 0) { - const item = activeItems[Math.floor(Math.random() * activeItems.length)]; - console.log(terminalLink(item.title, item.url)); - } - } - } catch { - // noop - } -} diff --git a/packages/schema/src/cli/config.ts b/packages/schema/src/cli/config.ts deleted file mode 100644 index 87fdd8e2f..000000000 --- a/packages/schema/src/cli/config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import fs from 'fs'; -import z, { ZodError } from 'zod'; -import { CliError } from './cli-error'; - -// TODO: future use -const schema = z.object({}); - -export type ConfigType = z.infer; - -export let config: ConfigType = schema.parse({}); - -/** - * Loads and validates CLI configuration file. - * @returns - */ -export function loadConfig(filename: string) { - try { - const fileData = fs.readFileSync(filename, `utf-8`); - const content = JSON.parse(fileData); - config = schema.parse(content); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - if (err?.code === `ENOENT`) { - throw new CliError(`Config file could not be found: ${filename}`); - } - if (err instanceof SyntaxError) { - throw new CliError(`Config is not a valid JSON file: ${filename}`); - } - if (err instanceof ZodError) { - throw new CliError(`Config file ${filename} is not valid: ${err}`); - } - throw new CliError(`Error loading config: ${filename}`); - } -} diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts deleted file mode 100644 index 62084ce9b..000000000 --- a/packages/schema/src/cli/index.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { ZModelLanguageMetaData } from '@zenstackhq/language/module'; -import colors from 'colors'; -import { Command, Option } from 'commander'; -import fs from 'fs'; -import telemetry from '../telemetry'; -import { getVersion } from '../utils/version-utils'; -import * as actions from './actions'; -import { loadConfig } from './config'; - -const DEFAULT_CONFIG_FILE = 'zenstack.config.json'; - -export const initAction = async (projectPath: string, options: Parameters[1]): Promise => { - await telemetry.trackSpan( - 'cli:command:start', - 'cli:command:complete', - 'cli:command:error', - { command: 'init' }, - () => actions.init(projectPath, options) - ); -}; - -export const infoAction = async (projectPath: string): Promise => { - await telemetry.trackSpan( - 'cli:command:start', - 'cli:command:complete', - 'cli:command:error', - { command: 'info' }, - () => actions.info(projectPath) - ); -}; - -export const generateAction = async (options: Parameters[1]): Promise => { - await telemetry.trackSpan( - 'cli:command:start', - 'cli:command:complete', - 'cli:command:error', - { command: 'generate' }, - () => actions.generate(process.cwd(), options) - ); -}; - -export const replAction = async (options: Parameters[1]): Promise => { - await telemetry.trackSpan( - 'cli:command:start', - 'cli:command:complete', - 'cli:command:error', - { command: 'repl' }, - () => actions.repl(process.cwd(), options) - ); -}; - -export const formatAction = async (options: Parameters[1]): Promise => { - await telemetry.trackSpan( - 'cli:command:start', - 'cli:command:complete', - 'cli:command:error', - { command: 'format' }, - () => actions.format(process.cwd(), options) - ); -}; - -export const checkAction = async (options: Parameters[1]): Promise => { - await telemetry.trackSpan( - 'cli:command:start', - 'cli:command:complete', - 'cli:command:error', - { command: 'check' }, - () => actions.check(process.cwd(), options) - ); -}; - -export function createProgram() { - const program = new Command('zenstack'); - - program.version(getVersion()!, '-v --version', 'display CLI version'); - - const schemaExtensions = ZModelLanguageMetaData.fileExtensions.join(', '); - - program - .description( - `${colors.bold.blue( - 'ζ' - )} 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.` - ); - - const pmOption = new Option('-p, --package-manager ', 'package manager to use').choices([ - 'npm', - 'yarn', - 'pnpm', - ]); - const noVersionCheckOption = new Option('--no-version-check', 'do not check for new version'); - const noDependencyCheckOption = new Option('--no-dependency-check', 'do not check if dependencies are installed'); - const offlineOption = new Option('--offline', 'run in offline mode'); - - program - .command('info') - .description('Get information of installed ZenStack and related packages.') - .argument('[path]', 'project path', '.') - .action(infoAction); - - program - .command('init') - .description('Initialize an existing project for ZenStack.') - .addOption(pmOption) - .addOption(new Option('--prisma ', 'location of Prisma schema file to bootstrap from')) - .addOption(new Option('--tag ', 'the NPM package tag to use when installing dependencies')) - .addOption(noVersionCheckOption) - .argument('[path]', 'project path', '.') - .action(initAction); - - program - .command('generate') - .description('Run code generation.') - .addOption(schemaOption) - .addOption(new Option('-o, --output ', 'default output directory for core plugins')) - .addOption(new Option('--with-plugins ', 'only run specific plugins')) - .addOption(new Option('--without-plugins ', 'exclude specific plugins')) - .addOption(new Option('--no-default-plugins', 'do not run default plugins')) - .addOption(new Option('--no-compile', 'do not compile the output of core plugins')) - .addOption(noVersionCheckOption) - .addOption(noDependencyCheckOption) - .addOption(offlineOption) - .action(generateAction); - - program - .command('repl') - .description('Start a REPL session.') - .option('--load-path ', 'path to load modules generated by ZenStack') - .option('--prisma-client ', 'path to Prisma client module') - .option('--debug', 'enable debug output') - .option('--table', 'enable table format output') - .action(replAction); - - program - .command('format') - .description('Format a ZenStack schema file.') - .addOption(schemaOption) - .option('--no-prisma-style', 'do not use prisma style') - .action(formatAction); - - program - .command('check') - .description('Check a ZenStack schema file for syntax or semantic errors.') - .addOption(schemaOption) - .action(checkAction); - - // make sure config is loaded before actions run - program.hook('preAction', async (_, actionCommand) => { - let configFile: string | undefined = actionCommand.opts().config; - - if (!configFile && fs.existsSync(DEFAULT_CONFIG_FILE)) { - configFile = DEFAULT_CONFIG_FILE; - } - - if (configFile) { - loadConfig(configFile); - } - }); - - return program; -} - -export default async function (): Promise { - await telemetry.trackSpan('cli:start', 'cli:complete', 'cli:error', { args: process.argv }, async () => { - const program = createProgram(); - - // handle errors explicitly to ensure telemetry - program.exitOverride(); - - await program.parseAsync(process.argv); - }); -} diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts deleted file mode 100644 index 7c9ffdd66..000000000 --- a/packages/schema/src/cli/plugin-runner.ts +++ /dev/null @@ -1,455 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-var-requires */ -import { DataModel, isPlugin, isTypeDef, Model, Plugin } from '@zenstackhq/language/ast'; -import { - createProject, - emitProject, - getDataModels, - getLiteral, - getLiteralArray, - hasValidationAttributes, - PluginError, - resolvePath, - saveProject, - type OptionValue, - type PluginDeclaredOptions, - type PluginFunction, - type PluginOptions, - type PluginResult, -} from '@zenstackhq/sdk'; -import { type DMMF } from '@zenstackhq/sdk/prisma'; -import colors from 'colors'; -import ora from 'ora'; -import path from 'path'; -import type { Project } from 'ts-morph'; -import { CorePlugins, ensureDefaultOutputFolder } from '../plugins/plugin-utils'; -import telemetry from '../telemetry'; -import { getVersion } from '../utils/version-utils'; - -type PluginInfo = { - name: string; - description?: string; - provider: string; - options: PluginDeclaredOptions; - run: PluginFunction; - dependencies: string[]; - module: any; -}; - -export type PluginRunnerOptions = { - schema: Model; - schemaPath: string; - output?: string; - withPlugins?: string[]; - withoutPlugins?: string[]; - defaultPlugins: boolean; - compile: boolean; -}; - -/** - * ZenStack plugin runner - */ -export class PluginRunner { - /** - * Runs a series of nested generators - */ - async run(runnerOptions: PluginRunnerOptions): Promise { - const version = getVersion(); - console.log(colors.bold(`⌛️ ZenStack CLI v${version}, running plugins`)); - - ensureDefaultOutputFolder(runnerOptions); - - const plugins: PluginInfo[] = []; - const pluginDecls = runnerOptions.schema.declarations.filter((d): d is Plugin => isPlugin(d)); - - for (const pluginDecl of pluginDecls) { - const pluginProvider = this.getPluginProvider(pluginDecl); - if (!pluginProvider) { - console.error(`Plugin ${pluginDecl.name} has invalid provider option`); - throw new PluginError('', `Plugin ${pluginDecl.name} has invalid provider option`); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let pluginModule: any; - - try { - pluginModule = this.loadPluginModule(pluginProvider, runnerOptions.schemaPath); - } catch (err) { - console.error(`Unable to load plugin module ${pluginProvider}: ${err}`); - throw new PluginError('', `Unable to load plugin module ${pluginProvider}`); - } - - if (!pluginModule.default || typeof pluginModule.default !== 'function') { - console.error(`Plugin provider ${pluginProvider} is missing a default function export`); - throw new PluginError('', `Plugin provider ${pluginProvider} is missing a default function export`); - } - - const dependencies = this.getPluginDependencies(pluginModule); - const pluginOptions: PluginDeclaredOptions = { - provider: pluginProvider, - }; - - pluginDecl.fields.forEach((f) => { - const value = getLiteral(f.value) ?? getLiteralArray(f.value); - if (value === undefined) { - throw new PluginError(pluginDecl.name, `Invalid option value for ${f.name}`); - } - pluginOptions[f.name] = value; - }); - - plugins.push({ - name: pluginDecl.name, - description: this.getPluginDescription(pluginModule), - provider: pluginProvider, - dependencies, - options: pluginOptions, - run: pluginModule.default as PluginFunction, - module: pluginModule, - }); - } - - const preprocessorPlugins = plugins.filter((p) => p.options.preprocessor); - const otherPlugins = plugins.filter((p) => !p.options.preprocessor); - - // calculate all plugins (including core plugins implicitly enabled) - const { corePlugins, userPlugins } = this.calculateAllPlugins(runnerOptions, otherPlugins); - const allPlugins = [...corePlugins, ...userPlugins]; - - // check dependencies - for (const plugin of allPlugins) { - for (const dep of plugin.dependencies) { - if (!allPlugins.find((p) => p.provider === dep)) { - console.error(`Plugin ${plugin.provider} depends on "${dep}" but it's not declared`); - throw new PluginError( - plugin.name, - `Plugin ${plugin.provider} depends on "${dep}" but it's not declared` - ); - } - } - } - - if (allPlugins.length === 0) { - console.log(colors.yellow('No plugins configured.')); - return; - } - - const warnings: string[] = []; - - // run core plugins first - let dmmf: DMMF.Document | undefined = undefined; - let shortNameMap: Map | undefined; - let prismaClientPath = '@prisma/client'; - let prismaClientDtsPath: string | undefined = undefined; - - const project = createProject(); - - const runUserPlugins = async (plugins: PluginInfo[]) => { - for (const { name, description, run, options: pluginOptions } of plugins) { - const options = { ...pluginOptions, prismaClientPath, prismaClientDtsPath }; - const r = await this.runPlugin( - name, - description, - run, - runnerOptions, - options as PluginOptions, - dmmf, - shortNameMap, - project, - false - ); - warnings.push(...(r?.warnings ?? [])); // the null-check is for backward compatibility - } - }; - - // run preprocessor plugins - await runUserPlugins(preprocessorPlugins); - - for (const { name, description, run, options: pluginOptions } of corePlugins) { - const options = { ...pluginOptions, prismaClientPath }; - const r = await this.runPlugin( - name, - description, - run, - runnerOptions, - options, - dmmf, - shortNameMap, - project, - true - ); - warnings.push(...(r?.warnings ?? [])); // the null-check is for backward compatibility - - if (r.dmmf) { - // use the DMMF returned by the plugin - dmmf = r.dmmf; - } - - if (r.shortNameMap) { - // use the model short name map returned by the plugin - shortNameMap = r.shortNameMap; - } - - if (r.prismaClientPath) { - // use the prisma client path returned by the plugin - prismaClientPath = r.prismaClientPath; - prismaClientDtsPath = r.prismaClientDtsPath; - } - } - - // compile code generated by core plugins - await compileProject(project, runnerOptions); - - // run user plugins - await runUserPlugins(userPlugins); - - console.log(colors.green(colors.bold('\n👻 All plugins completed successfully!'))); - warnings.forEach((w) => console.warn(colors.yellow(w))); - console.log(`Don't forget to restart your dev server to let the changes take effect.`); - } - - private calculateAllPlugins(options: PluginRunnerOptions, plugins: PluginInfo[]) { - const corePlugins: PluginInfo[] = []; - let zodImplicitlyAdded = false; - - // 1. @core/prisma - const existingPrisma = plugins.find((p) => p.provider === CorePlugins.Prisma); - if (existingPrisma) { - corePlugins.push(existingPrisma); - plugins.splice(plugins.indexOf(existingPrisma), 1); - } else if (options.defaultPlugins) { - corePlugins.push(this.makeCorePlugin(CorePlugins.Prisma, options.schemaPath, {})); - } - - const hasValidation = this.hasValidation(options.schema); - - // 2. @core/enhancer - const existingEnhancer = plugins.find((p) => p.provider === CorePlugins.Enhancer); - if (existingEnhancer) { - // enhancer should load zod schemas if there're validation rules - existingEnhancer.options.withZodSchemas = hasValidation; - corePlugins.push(existingEnhancer); - plugins.splice(plugins.indexOf(existingEnhancer), 1); - } else { - if (options.defaultPlugins) { - corePlugins.push( - this.makeCorePlugin(CorePlugins.Enhancer, options.schemaPath, { - // enhancer should load zod schemas if there're validation rules - withZodSchemas: hasValidation, - }) - ); - } - } - - // 3. @core/zod - const existingZod = plugins.find((p) => p.provider === CorePlugins.Zod); - if (existingZod && !existingZod.options.output) { - // we can reuse the user-provided zod plugin if it didn't specify a custom output path - plugins.splice(plugins.indexOf(existingZod), 1); - corePlugins.push(existingZod); - } - - if ( - !corePlugins.some((p) => p.provider === CorePlugins.Zod) && - options.defaultPlugins && - corePlugins.some((p) => p.provider === CorePlugins.Enhancer) && - hasValidation - ) { - // ensure "@core/zod" is enabled if "@core/enhancer" is enabled and there're validation rules - zodImplicitlyAdded = true; - corePlugins.push(this.makeCorePlugin(CorePlugins.Zod, options.schemaPath, { modelOnly: true })); - } - - // collect core plugins introduced by dependencies - plugins.forEach((plugin) => { - // TODO: generalize this - const isTrpcPlugin = - plugin.provider === '@zenstackhq/trpc' || - // for testing - (process.env.ZENSTACK_TEST && plugin.provider.includes('trpc')); - - for (const dep of plugin.dependencies) { - if (dep.startsWith('@core/')) { - const existing = corePlugins.find((p) => p.provider === dep); - if (existing) { - // TODO: generalize this - if (existing.provider === '@core/zod') { - // Zod plugin can be automatically enabled in `modelOnly` mode, however - // other plugin (tRPC) for now requires it to run in full mode - if (existing.options.modelOnly) { - delete existing.options.modelOnly; - } - - if ( - isTrpcPlugin && - zodImplicitlyAdded // don't do it for user defined zod plugin - ) { - // pass trpc plugin's `generateModels` option down to zod plugin - existing.options.generateModels = plugin.options.generateModels; - } - } - } else { - // add core dependency - const depOptions: Record = {}; - - // TODO: generalize this - if (dep === '@core/zod' && isTrpcPlugin) { - // pass trpc plugin's `generateModels` option down to zod plugin - depOptions.generateModels = plugin.options.generateModels; - } - - corePlugins.push(this.makeCorePlugin(dep, options.schemaPath, depOptions)); - } - } - } - }); - - return { corePlugins, userPlugins: plugins }; - } - - private makeCorePlugin( - provider: string, - schemaPath: string, - options: Record - ): PluginInfo { - const pluginModule = require(this.getPluginModulePath(provider, schemaPath)); - const pluginName = this.getPluginName(pluginModule, provider); - return { - name: pluginName, - description: this.getPluginDescription(pluginModule), - provider: provider, - dependencies: [], - options: { ...options, provider }, - run: pluginModule.default, - module: pluginModule, - }; - } - - private hasValidation(schema: Model) { - return getDataModels(schema).some((model) => hasValidationAttributes(model) || this.hasTypeDefFields(model)); - } - - private hasTypeDefFields(model: DataModel) { - return model.fields.some((f) => isTypeDef(f.type.reference?.ref)); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getPluginName(pluginModule: any, pluginProvider: string) { - return typeof pluginModule.name === 'string' ? (pluginModule.name as string) : pluginProvider; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getPluginDescription(pluginModule: any) { - return typeof pluginModule.description === 'string' ? (pluginModule.description as string) : undefined; - } - - private getPluginDependencies(pluginModule: any) { - return Array.isArray(pluginModule.dependencies) ? (pluginModule.dependencies as string[]) : []; - } - - private getPluginProvider(plugin: Plugin) { - const providerField = plugin.fields.find((f) => f.name === 'provider'); - return getLiteral(providerField?.value); - } - - private async runPlugin( - name: string, - description: string | undefined, - run: PluginFunction, - runnerOptions: PluginRunnerOptions, - options: PluginDeclaredOptions, - dmmf: DMMF.Document | undefined, - shortNameMap: Map | undefined, - project: Project, - isCorePlugin: boolean - ) { - if (!isCorePlugin && !this.isPluginEnabled(name, runnerOptions)) { - ora(`Plugin "${name}" is skipped`).start().warn(); - return { warnings: [] }; - } - - const title = description ?? `Running plugin ${colors.cyan(name)}`; - const spinner = ora(title).start(); - - try { - const r = await telemetry.trackSpan( - 'cli:plugin:start', - 'cli:plugin:complete', - 'cli:plugin:error', - { - plugin: name, - options, - }, - async () => { - const finalOptions = { - ...options, - schemaPath: runnerOptions.schemaPath, - shortNameMap, - } as PluginOptions; - return await run(runnerOptions.schema, finalOptions, dmmf, { - output: runnerOptions.output, - compile: runnerOptions.compile, - tsProject: project, - }); - } - ); - spinner.succeed(); - - if (typeof r === 'object') { - return r; - } else { - return { warnings: [] }; - } - } catch (err) { - spinner.fail(); - throw err; - } - } - - private isPluginEnabled(name: string, runnerOptions: PluginRunnerOptions) { - if (runnerOptions.withPlugins && !runnerOptions.withPlugins.includes(name)) { - return false; - } - - if (runnerOptions.withoutPlugins && runnerOptions.withoutPlugins.includes(name)) { - return false; - } - - return true; - } - - private getPluginModulePath(provider: string, schemaPath: string) { - if (process.env.ZENSTACK_TEST === '1' && provider.startsWith('@zenstackhq/')) { - // test code runs with its own sandbox of node_modules, make sure we don't - // accidentally resolve to the external ones - return path.resolve(`node_modules/${provider}`); - } - let pluginModulePath = provider; - if (provider.startsWith('@core/')) { - pluginModulePath = provider.replace(/^@core/, path.join(__dirname, '../plugins')); - } else { - try { - // direct require - require.resolve(pluginModulePath); - } catch { - // relative - pluginModulePath = resolvePath(provider, { schemaPath }); - } - } - return pluginModulePath; - } - - private loadPluginModule(provider: string, schemaPath: string) { - const pluginModulePath = this.getPluginModulePath(provider, schemaPath); - return require(pluginModulePath); - } -} - -async function compileProject(project: Project, runnerOptions: PluginRunnerOptions) { - if (!runnerOptions.output && runnerOptions.compile !== false) { - // emit - await emitProject(project); - } else { - // otherwise save ts files - await saveProject(project); - } -} diff --git a/packages/schema/src/constants.ts b/packages/schema/src/constants.ts deleted file mode 100644 index 586537af1..000000000 --- a/packages/schema/src/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -// replaced at build time -export const TELEMETRY_TRACKING_TOKEN = ''; diff --git a/packages/schema/src/extension.ts b/packages/schema/src/extension.ts deleted file mode 100644 index ad1c886e4..000000000 --- a/packages/schema/src/extension.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; - -import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node'; -import { AUTH_PROVIDER_ID, ZenStackAuthenticationProvider } from './vscode/zenstack-auth-provider'; -import { DocumentationCache } from './vscode/documentation-cache'; -import { ZModelPreview } from './vscode/zmodel-preview'; -import { ReleaseNotesManager } from './vscode/release-notes-manager'; -import telemetry from './vscode/vscode-telemetry'; - -// Global variables -let client: LanguageClient; - -// Utility to require authentication when needed -export async function requireAuth(): Promise { - let session: vscode.AuthenticationSession | undefined; - - session = await vscode.authentication.getSession(AUTH_PROVIDER_ID, [], { createIfNone: false }); - - if (!session) { - const signIn = 'Sign in'; - const selection = await vscode.window.showWarningMessage('Please sign in to use this feature', signIn); - telemetry.track('extension:signin:show'); - if (selection === signIn) { - telemetry.track('extension:signin:start'); - try { - session = await vscode.authentication.getSession(AUTH_PROVIDER_ID, [], { createIfNone: true }); - if (session) { - telemetry.track('extension:signin:complete'); - vscode.window.showInformationMessage('ZenStack sign-in successful!'); - } - } catch (e: unknown) { - telemetry.track('extension:signin:error', { error: e instanceof Error ? e.message : String(e) }); - vscode.window.showErrorMessage( - 'ZenStack sign-in failed: ' + (e instanceof Error ? e.message : String(e)) - ); - } - } - } - return session; -} - -// This function is called when the extension is activated. -export function activate(context: vscode.ExtensionContext): void { - telemetry.track('extension:activate'); - // Initialize and register the ZenStack authentication provider - context.subscriptions.push(new ZenStackAuthenticationProvider(context)); - - // Start language client - client = startLanguageClient(context); - - const documentationCache = new DocumentationCache(context); - context.subscriptions.push(documentationCache); - context.subscriptions.push(new ZModelPreview(context, client, documentationCache)); - context.subscriptions.push(new ReleaseNotesManager(context)); -} - -// This function is called when the extension is deactivated. -export function deactivate(): Thenable | undefined { - if (client) { - return client.stop(); - } - return undefined; -} - -function startLanguageClient(context: vscode.ExtensionContext): LanguageClient { - const serverModule = context.asAbsolutePath(path.join('bundle', 'language-server', 'main')); - // 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'}`, - ], - }; - - // If the extension is launched in debug mode then the debug server options are used - // Otherwise the run options are used - const serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { - module: serverModule, - transport: TransportKind.ipc, - options: debugOptions, - }, - }; - - const fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*.zmodel'); - context.subscriptions.push(fileSystemWatcher); - - // Options to control the language client - const clientOptions: LanguageClientOptions = { - documentSelector: [{ scheme: 'file', language: 'zmodel' }], - synchronize: { - // Notify the server about file changes to files contained in the workspace - fileEvents: fileSystemWatcher, - }, - }; - - // Create the language client and start the client. - const client = new LanguageClient('zmodel', 'ZenStack Model', serverOptions, clientOptions); - - // Start the client. This will also launch the server - void client.start(); - return client; -} diff --git a/packages/schema/src/global.d.ts b/packages/schema/src/global.d.ts deleted file mode 100644 index 16a5211ae..000000000 --- a/packages/schema/src/global.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'env' { - export const TELEMETRY_TRACKING_TOKEN: string; -} diff --git a/packages/schema/src/language-server/constants.ts b/packages/schema/src/language-server/constants.ts deleted file mode 100644 index 3c1a374c2..000000000 --- a/packages/schema/src/language-server/constants.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Supported Prisma db providers - */ -export const SUPPORTED_PROVIDERS = ['sqlite', 'postgresql', 'mysql', 'sqlserver', 'cockroachdb']; - -/** - * All scalar types - */ -export const SCALAR_TYPES = ['String', 'Int', 'Float', 'Decimal', 'BigInt', 'Boolean', 'Bytes', 'DateTime']; - -/** - * Name of standard library module - */ -export const STD_LIB_MODULE_NAME = 'stdlib.zmodel'; - -/** - * Name of module contributed by plugins - */ -export const PLUGIN_MODULE_NAME = 'plugin.zmodel'; - -/** - * Validation issues - */ -export enum IssueCodes { - MissingOppositeRelation = 'miss-opposite-relation', -} diff --git a/packages/schema/src/language-server/main.ts b/packages/schema/src/language-server/main.ts deleted file mode 100644 index 679012f2f..000000000 --- a/packages/schema/src/language-server/main.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { startLanguageServer } from 'langium'; -import { NodeFileSystem } from 'langium/node'; -import { createConnection, ProposedFeatures } from 'vscode-languageserver/node'; -import { URI } from 'vscode-uri'; -import { createZModelServices } from './zmodel-module'; -import { eagerLoadAllImports } from '../cli/cli-util'; - -// Create a connection to the client -const connection = createConnection(ProposedFeatures.all); - -// Inject the shared services and language-specific services -const { shared } = createZModelServices({ connection, ...NodeFileSystem }); - -// Add custom LSP request handlers -connection.onRequest('zenstack/getAllImportedZModelURIs', async (params: { textDocument: { uri: string } }) => { - try { - const uri = URI.parse(params.textDocument.uri); - const document = shared.workspace.LangiumDocuments.getOrCreateDocument(uri); - - // Ensure the document is parsed and built - if (!document.parseResult) { - await shared.workspace.DocumentBuilder.build([document]); - } - - // #region merge imported documents - const langiumDocuments = shared.workspace.LangiumDocuments; - - // load all imports - const importedURIs = eagerLoadAllImports(document, langiumDocuments); - - const importedDocuments = importedURIs.map((uri) => langiumDocuments.getOrCreateDocument(uri)); - - // build the document together with standard library, plugin modules, and imported documents - await shared.workspace.DocumentBuilder.build([document, ...importedDocuments], { - validationChecks: 'all', - }); - - const hasSyntaxErrors = [uri, ...importedURIs].some((uri) => { - const doc = langiumDocuments.getOrCreateDocument(uri); - return ( - doc.parseResult.lexerErrors.length > 0 || - doc.parseResult.parserErrors.length > 0 || - doc.diagnostics?.some((e) => e.severity === 1) - ); - }); - - return { - hasSyntaxErrors, - importedURIs, - }; - } catch (error) { - console.error('Error getting imported ZModel file:', error); - return { - hasSyntaxErrors: true, - importedURIs: [], - }; - } -}); - -// Start the language server with the shared services -startLanguageServer(shared); diff --git a/packages/schema/src/language-server/types.ts b/packages/schema/src/language-server/types.ts deleted file mode 100644 index b7420ba09..000000000 --- a/packages/schema/src/language-server/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AstNode, ValidationAcceptor } from 'langium'; - -/** - * AST validator contract - */ -export interface AstValidator { - /** - * Validates an AST node - */ - validate(node: T, accept: ValidationAcceptor): void; -} diff --git a/packages/schema/src/language-server/utils.ts b/packages/schema/src/language-server/utils.ts deleted file mode 100644 index 2d31975a6..000000000 --- a/packages/schema/src/language-server/utils.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - isArrayExpr, - isDataModel, - isReferenceExpr, - isTypeDef, - TypeDef, - type DataModel, - type DataModelField, - type ReferenceExpr, -} from '@zenstackhq/language/ast'; -import { resolved } from '@zenstackhq/sdk'; - -/** - * Gets lists of unique fields declared at the data model level - */ -export function getUniqueFields(model: DataModel) { - const uniqueAttrs = model.attributes.filter( - (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'); - if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { - return []; - } - - return fieldsArg.value.items - .filter((item): item is ReferenceExpr => isReferenceExpr(item)) - .map((item) => resolved(item.target) as DataModelField); - }); -} - -/** - * Checks if the given node can contain resolvable members. - */ -export function isMemberContainer(node: unknown): node is DataModel | TypeDef { - return isDataModel(node) || isTypeDef(node); -} diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts deleted file mode 100644 index 0efa760b8..000000000 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ /dev/null @@ -1,411 +0,0 @@ -import { - ArrayExpr, - Attribute, - AttributeArg, - AttributeParam, - DataModelAttribute, - DataModelField, - DataModelFieldAttribute, - InternalAttribute, - ReferenceExpr, - isArrayExpr, - isAttribute, - isDataModel, - isDataModelField, - isEnum, - isReferenceExpr, - isTypeDef, - isTypeDefField, -} from '@zenstackhq/language/ast'; -import { - hasAttribute, - isDataModelFieldReference, - isDelegateModel, - isFutureExpr, - isRelationshipField, - resolved, -} from '@zenstackhq/sdk'; -import { ValidationAcceptor, streamAllContents, streamAst } from 'langium'; -import pluralize from 'pluralize'; -import { AstValidator } from '../types'; -import { getStringLiteral, mapBuiltinTypeToExpressionType, typeAssignable } from './utils'; - -// a registry of function handlers marked with @check -const attributeCheckers = new Map(); - -// function handler decorator -function check(name: string) { - return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) { - if (!attributeCheckers.get(name)) { - attributeCheckers.set(name, descriptor); - } - return descriptor; - }; -} - -type AttributeApplication = DataModelAttribute | DataModelFieldAttribute | InternalAttribute; - -/** - * Validates function declarations. - */ -export default class AttributeApplicationValidator implements AstValidator { - validate(attr: AttributeApplication, accept: ValidationAcceptor) { - const decl = attr.decl.ref; - if (!decl) { - return; - } - - const targetDecl = attr.$container; - if (decl.name === '@@@targetField' && !isAttribute(targetDecl)) { - 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 (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 }); - } - - const filledParams = new Set(); - - for (const arg of attr.args) { - let paramDecl: AttributeParam | undefined; - if (!arg.name) { - paramDecl = decl.params.find((p) => p.default && !filledParams.has(p)); - if (!paramDecl) { - accept('error', `Unexpected unnamed argument`, { - node: arg, - }); - return; - } - } 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, - }); - return; - } - } - - if (!assignableToAttributeParam(arg, paramDecl, attr)) { - accept('error', `Value is not assignable to parameter`, { - node: arg, - }); - return; - } - - if (filledParams.has(paramDecl)) { - 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)); - if (missingParams.length > 0) { - accept( - 'error', - `Required ${pluralize('parameter', missingParams.length)} not provided: ${missingParams - .map((p) => p.name) - .join(', ')}`, - { node: attr } - ); - return; - } - - // run checkers for specific attributes - const checker = attributeCheckers.get(decl.name); - if (checker) { - checker.value.call(this, attr, accept); - } - } - - @check('@@allow') - @check('@@deny') - private _checkModelLevelPolicy(attr: AttributeApplication, accept: ValidationAcceptor) { - const kind = getStringLiteral(attr.args[0].value); - if (!kind) { - accept('error', `expects a string literal`, { node: attr.args[0] }); - return; - } - this.validatePolicyKinds(kind, ['create', 'read', 'update', 'delete', 'all'], attr, accept); - - // @encrypted fields cannot be used in policy rules - this.rejectEncryptedFields(attr, accept); - } - - @check('@allow') - @check('@deny') - private _checkFieldLevelPolicy(attr: AttributeApplication, accept: ValidationAcceptor) { - const kind = getStringLiteral(attr.args[0].value); - if (!kind) { - accept('error', `expects a string literal`, { node: attr.args[0] }); - return; - } - const kindItems = this.validatePolicyKinds(kind, ['read', 'update', 'all'], attr, accept); - - const expr = attr.args[1].value; - if (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 - if (kindItems.includes('update') || kindItems.includes('all')) { - const field = attr.$container as DataModelField; - if (isRelationshipField(field)) { - 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 } - ); - } - } - - // @encrypted fields cannot be used in policy rules - this.rejectEncryptedFields(attr, accept); - } - - @check('@@validate') - private _checkValidate(attr: AttributeApplication, accept: ValidationAcceptor) { - const condition = attr.args[0]?.value; - if ( - condition && - streamAst(condition).some( - (node) => isDataModelFieldReference(node) && isDataModel(node.$resolvedType?.decl) - ) - ) { - accept('error', `\`@@validate\` condition cannot use relation fields`, { node: condition }); - } - } - - @check('@@unique') - private _checkUnique(attr: AttributeApplication, accept: ValidationAcceptor) { - const fields = attr.args[0]?.value; - if (fields && isArrayExpr(fields)) { - fields.items.forEach((item) => { - if (!isReferenceExpr(item)) { - accept('error', `Expecting a field reference`, { node: item }); - return; - } - if (!isDataModelField(item.target.ref)) { - accept('error', `Expecting a field reference`, { node: item }); - 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, - }); - } - }); - } else { - accept('error', `Expected an array of field references`, { node: fields }); - } - } - - @check('@regex') - private _checkRegex(attr: AttributeApplication, accept: ValidationAcceptor) { - const regex = getStringLiteral(attr.args[0]?.value); - if (regex === undefined) { - accept('error', `Expecting a string literal`, { node: attr.args[0] ?? attr }); - return; - } - - try { - new RegExp(regex); - } catch (e) { - accept('error', `${e}`, { node: attr.args[0] }); - } - } - - private rejectEncryptedFields(attr: AttributeApplication, accept: ValidationAcceptor) { - 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 }); - } - }); - } - - private validatePolicyKinds( - kind: string, - candidates: string[], - attr: AttributeApplication, - 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 } - ); - } - }); - return items; - } -} - -function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, attr: AttributeApplication): boolean { - const argResolvedType = arg.$resolvedType; - if (!argResolvedType) { - return false; - } - - let dstType = param.type.type; - let dstIsArray = param.type.array; - - if (dstType === 'ContextType') { - // ContextType is inferred from the attribute's container's type - if (isDataModelField(attr.$container)) { - // If the field is Typed JSON, and the param is @default, the argument must be a string - const dstIsTypedJson = hasAttribute(attr.$container, '@json'); - if (dstIsTypedJson && param.default) { - return argResolvedType.decl === 'String'; - } - - dstIsArray = attr.$container.type.array; - } - } - - const dstRef = param.type.reference; - - if (dstType === 'Any' && !dstIsArray) { - return true; - } - - if (argResolvedType.decl === 'Any') { - // arg is any type - if (!argResolvedType.array) { - // if it's not an array, it's assignable to any type - return true; - } else { - // otherwise it's assignable to any array type - return argResolvedType.array === dstIsArray; - } - } - - // destination is field reference or transitive field reference, check if - // argument is reference or array or reference - if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') { - if (dstIsArray) { - return ( - isArrayExpr(arg.value) && - !arg.value.items.find((item) => !isReferenceExpr(item) || !isDataModelField(item.target.ref)) - ); - } else { - return isReferenceExpr(arg.value) && isDataModelField(arg.value.target.ref); - } - } - - if (isEnum(argResolvedType.decl)) { - // enum type - - let attrArgDeclType = dstRef?.ref; - 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; - } else if (dstType) { - // scalar type - - if (typeof argResolvedType?.decl !== 'string') { - // destination type is not a reference, so argument type must be a plain expression - return false; - } - - if (dstType === 'ContextType') { - // attribute parameter type is ContextType, need to infer type from - // the attribute's container - if (isDataModelField(attr.$container)) { - if (!attr.$container?.type?.type) { - return false; - } - - dstType = mapBuiltinTypeToExpressionType(attr.$container.type.type); - dstIsArray = attr.$container.type.array; - } else { - dstType = 'Any'; - } - } - - return typeAssignable(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array; - } else { - // reference type - 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'); - if (!targetField) { - // no field type constraint - return true; - } - - const fieldTypes = (targetField.args[0].value as ArrayExpr).items.map( - (item) => (item as ReferenceExpr).target.ref?.name - ); - - let allowed = false; - for (const allowedType of fieldTypes) { - switch (allowedType) { - case 'StringField': - allowed = allowed || targetDecl.type.type === 'String'; - break; - case 'IntField': - allowed = allowed || targetDecl.type.type === 'Int'; - break; - case 'BigIntField': - allowed = allowed || targetDecl.type.type === 'BigInt'; - break; - case 'FloatField': - allowed = allowed || targetDecl.type.type === 'Float'; - break; - case 'DecimalField': - allowed = allowed || targetDecl.type.type === 'Decimal'; - break; - case 'BooleanField': - allowed = allowed || targetDecl.type.type === 'Boolean'; - break; - case 'DateTimeField': - allowed = allowed || targetDecl.type.type === 'DateTime'; - break; - case 'JsonField': - allowed = allowed || targetDecl.type.type === 'Json'; - break; - case 'BytesField': - allowed = allowed || targetDecl.type.type === 'Bytes'; - break; - case 'ModelField': - allowed = allowed || isDataModel(targetDecl.type.reference?.ref); - break; - case 'TypeDefField': - allowed = allowed || isTypeDef(targetDecl.type.reference?.ref); - break; - default: - break; - } - if (allowed) { - break; - } - } - - return allowed; -} - -export function validateAttributeApplication(attr: AttributeApplication, accept: ValidationAcceptor) { - new AttributeApplicationValidator().validate(attr, accept); -} diff --git a/packages/schema/src/language-server/validator/attribute-validator.ts b/packages/schema/src/language-server/validator/attribute-validator.ts deleted file mode 100644 index 1bf961159..000000000 --- a/packages/schema/src/language-server/validator/attribute-validator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Attribute } from '@zenstackhq/language/ast'; -import { ValidationAcceptor } from 'langium'; -import { AstValidator } from '../types'; -import { validateAttributeApplication } from './attribute-application-validator'; - -/** - * Validates attribute declarations. - */ -export default class AttributeValidator implements AstValidator { - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function - validate(attr: Attribute, accept: ValidationAcceptor): void { - attr.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); - } -} diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts deleted file mode 100644 index 8b8400b4e..000000000 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ /dev/null @@ -1,446 +0,0 @@ -import { - ArrayExpr, - DataModel, - DataModelField, - ReferenceExpr, - isDataModel, - isEnum, - isStringLiteral, - isTypeDef, -} from '@zenstackhq/language/ast'; -import { - getModelFieldsWithBases, - getModelIdFields, - getModelUniqueFields, - hasAttribute, - isDelegateModel, -} from '@zenstackhq/sdk'; -import { AstNode, DiagnosticInfo, ValidationAcceptor, getDocument } from 'langium'; -import { findUpInheritance } from '../../utils/ast-utils'; -import { IssueCodes, SCALAR_TYPES } from '../constants'; -import { AstValidator } from '../types'; -import { getUniqueFields } from '../utils'; -import { validateAttributeApplication } from './attribute-application-validator'; -import { validateDuplicatedDeclarations } from './utils'; - -/** - * Validates data model declarations. - */ -export default class DataModelValidator implements AstValidator { - validate(dm: DataModel, accept: ValidationAcceptor): void { - this.validateBaseAbstractModel(dm, accept); - this.validateBaseDelegateModel(dm, accept); - validateDuplicatedDeclarations(dm, getModelFieldsWithBases(dm), accept); - this.validateAttributes(dm, accept); - this.validateFields(dm, accept); - - if (dm.superTypes.length > 0) { - this.validateInheritance(dm, accept); - } - } - - 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 modelLevelIds = getModelIdFields(dm); - const modelUniqueFields = getModelUniqueFields(dm); - - if ( - !dm.isAbstract && - !dm.isView && - idFields.length === 0 && - modelLevelIds.length === 0 && - uniqueFields.length === 0 && - modelUniqueFields.length === 0 - ) { - accept( - 'error', - '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, - }); - } else if (idFields.length > 1) { - accept('error', 'Model can include at most one field with @id attribute', { - node: dm, - }); - } else { - 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 }); - } - - const isArray = idField.type.array; - const isScalar = SCALAR_TYPES.includes(idField.type.type as (typeof SCALAR_TYPES)[number]); - const isValidType = isScalar || isEnum(idField.type.reference?.ref); - - if (isArray || !isValidType) { - accept('error', 'Field with @id attribute must be of scalar or enum type', { node: idField }); - } - }); - } - - dm.fields.forEach((field) => this.validateField(field, accept)); - - if (!dm.isAbstract) { - allFields - .filter((x) => isDataModel(x.type.reference?.ref)) - .forEach((y) => { - this.validateRelationField(dm, y, accept); - }); - } - } - - 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 }); - } - - if (field.type.unsupported && !isStringLiteral(field.type.unsupported.value)) { - accept('error', 'Unsupported type argument must be a string literal', { node: field.type.unsupported }); - } - - 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 }); - } - } - } - - private validateAttributes(dm: DataModel, accept: ValidationAcceptor) { - dm.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); - } - - private parseRelation(field: DataModelField, accept?: ValidationAcceptor) { - const relAttr = field.attributes.find((attr) => attr.decl.ref?.name === '@relation'); - - let name: string | undefined; - let fields: ReferenceExpr[] | undefined; - let references: ReferenceExpr[] | undefined; - let valid = true; - - if (!relAttr) { - return { attr: relAttr, name, fields, references, valid: true }; - } - - for (const arg of relAttr.args) { - if (!arg.name || arg.name === 'name') { - if (isStringLiteral(arg.value)) { - name = arg.value.value as string; - } - } else if (arg.name === 'fields') { - fields = (arg.value as ArrayExpr).items as ReferenceExpr[]; - if (fields.length === 0) { - if (accept) { - accept('error', `"fields" value cannot be empty`, { - node: arg, - }); - } - valid = false; - } - } else if (arg.name === 'references') { - references = (arg.value as ArrayExpr).items as ReferenceExpr[]; - if (references.length === 0) { - if (accept) { - accept('error', `"references" value cannot be empty`, { - node: arg, - }); - } - valid = false; - } - } - } - - if (!fields && !references) { - return { attr: relAttr, name, fields, references, valid: true }; - } - - if (!fields || !references) { - if (accept) { - 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 }); - } - } else { - for (let i = 0; i < fields.length; i++) { - if (!field.type.optional && fields[i].$resolvedType?.nullable) { - // if relation is not optional, then fk field must not be nullable - if (accept) { - accept( - 'error', - `relation "${field.name}" is not optional, but field "${fields[i].target.$refText}" is optional`, - { node: fields[i].target.ref! } - ); - } - } - - if (!fields[i].$resolvedType) { - if (accept) { - accept('error', `field reference is unresolved`, { node: fields[i] }); - } - } - if (!references[i].$resolvedType) { - if (accept) { - accept('error', `field reference is unresolved`, { node: references[i] }); - } - } - - if ( - fields[i].$resolvedType?.decl !== references[i].$resolvedType?.decl || - fields[i].$resolvedType?.array !== references[i].$resolvedType?.array - ) { - if (accept) { - accept('error', `values of "references" and "fields" must have the same type`, { - node: relAttr, - }); - } - } - } - } - } - - return { attr: relAttr, name, fields, references, valid }; - } - - private isSelfRelation(field: DataModelField) { - return field.type.reference?.ref === field.$container; - } - - private validateRelationField(contextModel: DataModel, field: DataModelField, accept: ValidationAcceptor) { - const thisRelation = this.parseRelation(field, accept); - if (!thisRelation.valid) { - return; - } - - if (this.isFieldInheritedFromDelegateModel(field, contextModel)) { - // relation fields inherited from delegate model don't need opposite relation - 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 - ); - oppositeFields = oppositeFields.filter((f) => { - const fieldRel = this.parseRelation(f); - return fieldRel.valid && fieldRel.name === thisRelation.name; - }); - - if (oppositeFields.length === 0) { - const info: DiagnosticInfo = { - node: field, - code: IssueCodes.MissingOppositeRelation, - }; - - info.property = 'name'; - const container = field.$container; - - const relationFieldDocUri = getDocument(container).textDocument.uri; - const relationDataModelName = container.name; - - const data: MissingOppositeRelationData = { - relationFieldName: field.name, - relationDataModelName, - relationFieldDocUri, - dataModelName: contextModel.name, - }; - - info.data = data; - - accept( - 'error', - `The relation field "${field.name}" on model "${contextModel.name}" is missing an opposite relation field on model "${oppositeModel.name}"`, - info - ); - return; - } else if (oppositeFields.length > 1) { - oppositeFields - .filter((f) => f.$container !== contextModel) - .forEach((f) => { - if (this.isSelfRelation(f)) { - // self relations are partial - // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations - } else { - accept( - 'error', - `Fields ${oppositeFields.map((f) => '"' + f.name + '"').join(', ')} on model "${ - oppositeModel.name - }" refer to the same relation to model "${field.$container.name}"`, - { node: f } - ); - } - }); - return; - } - - const oppositeField = oppositeFields[0]; - const oppositeRelation = this.parseRelation(oppositeField); - - let relationOwner: DataModelField; - - 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, - }); - return; - } else { - relationOwner = oppositeField; - } - } 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, - }); - return; - } else { - relationOwner = field; - } - } else { - // if both the field is array, then it's an implicit many-to-many relation - if (!(field.type.array && oppositeField.type.array)) { - [field, oppositeField].forEach((f) => { - if (!this.isSelfRelation(f)) { - accept( - 'error', - 'Field for one side of relation must carry @relation attribute with both "fields" and "references"', - { node: f } - ); - } - }); - } - return; - } - - if (!relationOwner.type.array && !relationOwner.type.optional) { - accept('error', 'Relation field needs to be list or optional', { - node: relationOwner, - }); - return; - } - - if (relationOwner !== field && !relationOwner.type.array) { - // one-to-one relation requires defining side's reference field to be @unique - // e.g.: - // model User { - // id String @id @default(cuid()) - // data UserData? - // } - // model UserData { - // id String @id @default(cuid()) - // user User @relation(fields: [userId], references: [id]) - // userId String - // } - // - // UserData.userId field needs to be @unique - - const containingModel = field.$container as DataModel; - const uniqueFieldList = getUniqueFields(containingModel); - - // field is defined in the abstract base model - if (containingModel !== contextModel) { - uniqueFieldList.push(...getUniqueFields(contextModel)); - } - - 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')) { - return; - } - 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 } - ); - } - }); - } - } - - // 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); - if (basePath && basePath.some(isDelegateModel)) { - return true; - } else { - return false; - } - } - - 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') - ) - accept( - 'error', - `Model ${superType.$refText} cannot be extended because it's neither abstract nor marked as "@@delegate"`, - { - 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 validateInheritance(dm: DataModel, accept: ValidationAcceptor) { - const seen = [dm]; - 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}`, - { - node: dm, - } - ); - return; - } - seen.push(current); - todo.push(...current.superTypes.map((superType) => superType.ref!)); - } - } -} - -export interface MissingOppositeRelationData { - relationDataModelName: string; - relationFieldName: string; - // it might be the abstract model in the imported document - relationFieldDocUri: string; - - // the name of DataModel that the relation field belongs to. - // the document is the same with the error node. - dataModelName: string; -} diff --git a/packages/schema/src/language-server/validator/datasource-validator.ts b/packages/schema/src/language-server/validator/datasource-validator.ts deleted file mode 100644 index bf934a085..000000000 --- a/packages/schema/src/language-server/validator/datasource-validator.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { DataSource, isInvocationExpr } from '@zenstackhq/language/ast'; -import { AstValidator } from '../types'; -import { ValidationAcceptor } from 'langium'; -import { getStringLiteral, validateDuplicatedDeclarations } from './utils'; -import { SUPPORTED_PROVIDERS } from '../constants'; - -/** - * Validates data source declarations. - */ -export default class DataSourceValidator implements AstValidator { - validate(ds: DataSource, accept: ValidationAcceptor): void { - validateDuplicatedDeclarations(ds, ds.fields, accept); - this.validateProvider(ds, accept); - this.validateUrl(ds, accept); - this.validateRelationMode(ds, accept); - } - - private validateProvider(ds: DataSource, accept: ValidationAcceptor) { - const provider = ds.fields.find((f) => f.name === 'provider'); - if (!provider) { - accept('error', 'datasource must include a "provider" field', { - node: ds, - }); - return; - } - - const value = getStringLiteral(provider.value); - if (!value) { - accept('error', '"provider" must be set to a string literal', { - node: provider.value, - }); - } else if (!SUPPORTED_PROVIDERS.includes(value)) { - accept( - 'error', - `Provider "${value}" is not supported. Choose from ${SUPPORTED_PROVIDERS.map((p) => '"' + p + '"').join( - ' | ' - )}.`, - { node: provider.value } - ); - } - } - - private validateUrl(ds: DataSource, accept: ValidationAcceptor) { - const url = ds.fields.find((f) => f.name === 'url'); - if (!url) { - // url field is optional for new versions of Prisma - return; - } - - for (const fieldName of ['url', 'shadowDatabaseUrl']) { - const field = ds.fields.find((f) => f.name === fieldName); - if (!field) { - 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, - }); - } - } - } - - private validateRelationMode(ds: DataSource, accept: ValidationAcceptor) { - const field = ds.fields.find((f) => f.name === 'relationMode'); - 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 }); - } - } - } -} diff --git a/packages/schema/src/language-server/validator/enum-validator.ts b/packages/schema/src/language-server/validator/enum-validator.ts deleted file mode 100644 index 5780d91fb..000000000 --- a/packages/schema/src/language-server/validator/enum-validator.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Enum, EnumField } from '@zenstackhq/language/ast'; -import { ValidationAcceptor } from 'langium'; -import { AstValidator } from '../types'; -import { validateAttributeApplication } from './attribute-application-validator'; -import { validateDuplicatedDeclarations } from './utils'; - -/** - * 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); - _enum.fields.forEach((field) => { - this.validateField(field, accept); - }); - } - - private validateAttributes(_enum: Enum, accept: ValidationAcceptor) { - _enum.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); - } - - private validateField(field: EnumField, accept: ValidationAcceptor) { - field.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); - } -} diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts deleted file mode 100644 index 06083e9e4..000000000 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { - AstNode, - BinaryExpr, - DataModelAttribute, - Expression, - ExpressionType, - isArrayExpr, - isDataModel, - isDataModelAttribute, - isDataModelField, - isEnum, - isLiteralExpr, - isMemberAccessExpr, - isNullExpr, - isReferenceExpr, - isThisExpr, -} from '@zenstackhq/language/ast'; -import { - getAttributeArgLiteral, - isAuthInvocation, - isDataModelFieldReference, - isEnumFieldReference, -} from '@zenstackhq/sdk'; -import { ValidationAcceptor, streamAst } from 'langium'; -import { findUpAst, getContainingDataModel } from '../../utils/ast-utils'; -import { AstValidator } from '../types'; -import { isAuthOrAuthMemberAccess, typeAssignable } from './utils'; - -/** - * Validates expressions. - */ -export default class ExpressionValidator implements AstValidator { - validate(expr: Expression, accept: ValidationAcceptor): void { - // deal with a few cases where reference resolution fail silently - if (!expr.$resolvedType) { - if (isAuthInvocation(expr)) { - // check was done at link time - accept( - 'error', - 'auth() cannot be resolved because no model marked with "@@auth()" or named "User" is found', - { node: expr } - ); - } else { - const hasReferenceResolutionError = streamAst(expr).some((node) => { - if (isMemberAccessExpr(node)) { - return !!node.member.error; - } - if (isReferenceExpr(node)) { - return !!node.target.error; - } - return false; - }); - if (!hasReferenceResolutionError) { - // report silent errors not involving linker errors - accept('error', 'Expression cannot be resolved', { - node: expr, - }); - } - } - } - - // extra validations by expression type - switch (expr.$type) { - case 'BinaryExpr': - this.validateBinaryExpr(expr, accept); - break; - } - } - - 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 (!expr.right.$resolvedType?.array) { - accept('error', 'right operand of "in" must be an array', { - node: expr.right, - }); - } - - this.validateCrossModelFieldComparison(expr, accept); - break; - } - - case '>': - case '>=': - case '<': - case '<=': - case '&&': - case '||': { - if (expr.left.$resolvedType?.array) { - accept('error', 'operand cannot be an array', { node: expr.left }); - break; - } - - if (expr.right.$resolvedType?.array) { - accept('error', 'operand cannot be an array', { node: expr.right }); - break; - } - - let supportedShapes: ExpressionType[]; - if (['>', '>=', '<', '<='].includes(expr.operator)) { - supportedShapes = ['Int', 'Float', 'DateTime', 'Any']; - } else { - supportedShapes = ['Boolean', 'Any']; - } - - if ( - typeof expr.left.$resolvedType?.decl !== 'string' || - !supportedShapes.includes(expr.left.$resolvedType.decl) - ) { - 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, - }); - return; - } - - // DateTime comparison is only allowed between two DateTime values - if (expr.left.$resolvedType.decl === 'DateTime' && expr.right.$resolvedType.decl !== 'DateTime') { - accept('error', 'incompatible operand types', { node: expr }); - } else if ( - expr.right.$resolvedType.decl === 'DateTime' && - expr.left.$resolvedType.decl !== 'DateTime' - ) { - accept('error', 'incompatible operand types', { node: expr }); - } - - if (expr.operator !== '&&' && expr.operator !== '||') { - this.validateCrossModelFieldComparison(expr, accept); - } - break; - } - - case '==': - case '!=': { - if (this.isInValidationContext(expr)) { - // 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)) - ) { - return; - } - } - - if (!!expr.left.$resolvedType?.array !== !!expr.right.$resolvedType?.array) { - accept('error', 'incompatible operand types', { node: expr }); - break; - } - - if (!this.validateCrossModelFieldComparison(expr, accept)) { - break; - } - - if ( - (expr.left.$resolvedType?.nullable && isNullExpr(expr.right)) || - (expr.right.$resolvedType?.nullable && isNullExpr(expr.left)) - ) { - // comparing nullable field with null - return; - } - - if ( - typeof expr.left.$resolvedType?.decl === 'string' && - typeof expr.right.$resolvedType?.decl === 'string' - ) { - // scalar types assignability - if ( - !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 }); - } - return; - } - - // disallow comparing model type with scalar type or comparison between - // incompatible model types - const leftType = expr.left.$resolvedType?.decl; - const rightType = expr.right.$resolvedType?.decl; - if (isDataModel(leftType) && isDataModel(rightType)) { - if (leftType != rightType) { - // incompatible model types - // TODO: inheritance case? - accept('error', 'incompatible operand types', { node: expr }); - } - - // not supported: - // - foo == bar - // - foo == this - if ( - isDataModelFieldReference(expr.left) && - (isThisExpr(expr.right) || isDataModelFieldReference(expr.right)) - ) { - accept('error', 'comparison between model-typed fields are not supported', { node: expr }); - } else if ( - isDataModelFieldReference(expr.right) && - (isThisExpr(expr.left) || isDataModelFieldReference(expr.left)) - ) { - accept('error', 'comparison between model-typed fields are not supported', { node: expr }); - } - } else if ( - (isDataModel(leftType) && !isNullExpr(expr.right)) || - (isDataModel(rightType) && !isNullExpr(expr.left)) - ) { - // comparing model against scalar (except null) - accept('error', 'incompatible operand types', { node: expr }); - } - break; - } - - case '?': - case '!': - case '^': - this.validateCollectionPredicate(expr, accept); - break; - } - } - - private validateCrossModelFieldComparison(expr: BinaryExpr, accept: ValidationAcceptor) { - // not supported in "read" rules: - // - foo.a == bar - // - foo.user.id == userId - // except: - // - future().userId == userId - if ( - (isMemberAccessExpr(expr.left) && - isDataModelField(expr.left.member.ref) && - expr.left.member.ref.$container != getContainingDataModel(expr)) || - (isMemberAccessExpr(expr.right) && - isDataModelField(expr.right.member.ref) && - expr.right.member.ref.$container != getContainingDataModel(expr)) - ) { - // foo.user.id == auth().id - // foo.user.id == "123" - // foo.user.id == null - // foo.user.id == EnumValue - if (!(this.isNotModelFieldExpr(expr.left) || this.isNotModelFieldExpr(expr.right))) { - const containingPolicyAttr = findUpAst( - expr, - (node) => isDataModelAttribute(node) && ['@@allow', '@@deny'].includes(node.decl.$refText) - ) as DataModelAttribute | undefined; - - if (containingPolicyAttr) { - const operation = getAttributeArgLiteral(containingPolicyAttr, 'operation'); - if (operation?.split(',').includes('all') || operation?.split(',').includes('read')) { - accept( - 'error', - 'comparison between fields of different models is not supported in model-level "read" rules', - { - node: expr, - } - ); - return false; - } - } - } - } - - return true; - } - - 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 }); - return; - } - } - - private isInValidationContext(node: AstNode) { - return findUpAst(node, (n) => isDataModelAttribute(n) && n.decl.$refText === '@@validate'); - } - - private isNotModelFieldExpr(expr: Expression): boolean { - return ( - // literal - isLiteralExpr(expr) || - // enum field - isEnumFieldReference(expr) || - // null - isNullExpr(expr) || - // `auth()` access - isAuthOrAuthMemberAccess(expr) || - // array - (isArrayExpr(expr) && expr.items.every((item) => this.isNotModelFieldExpr(item))) - ); - } -} diff --git a/packages/schema/src/language-server/validator/function-decl-validator.ts b/packages/schema/src/language-server/validator/function-decl-validator.ts deleted file mode 100644 index 9ef56c468..000000000 --- a/packages/schema/src/language-server/validator/function-decl-validator.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { FunctionDecl } from '@zenstackhq/language/ast'; -import { ValidationAcceptor } from 'langium'; -import { AstValidator } from '../types'; -import { validateAttributeApplication } from './attribute-application-validator'; - -/** - * Validates function declarations. - */ -export default class FunctionDeclValidator implements AstValidator { - validate(funcDecl: FunctionDecl, accept: ValidationAcceptor) { - funcDecl.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); - } -} diff --git a/packages/schema/src/language-server/validator/function-invocation-validator.ts b/packages/schema/src/language-server/validator/function-invocation-validator.ts deleted file mode 100644 index eff614e3c..000000000 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { - Argument, - DataModel, - DataModelAttribute, - DataModelFieldAttribute, - Expression, - FunctionDecl, - FunctionParam, - InvocationExpr, - isArrayExpr, - isDataModel, - isDataModelAttribute, - isDataModelFieldAttribute, - isInvocationExpr, - isLiteralExpr, -} from '@zenstackhq/language/ast'; -import { - ExpressionContext, - getFieldReference, - getFunctionExpressionContext, - getLiteral, - isDataModelFieldReference, - isEnumFieldReference, - isFromStdlib, - isValidationAttribute, -} from '@zenstackhq/sdk'; -import { AstNode, streamAst, ValidationAcceptor } from 'langium'; -import { match, P } from 'ts-pattern'; -import { isCheckInvocation } from '../../utils/ast-utils'; -import { AstValidator } from '../types'; -import { isAuthOrAuthMemberAccess, typeAssignable } from './utils'; - -// a registry of function handlers marked with @func -const invocationCheckers = new Map(); - -// function handler decorator -function func(name: string) { - return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) { - if (!invocationCheckers.get(name)) { - invocationCheckers.set(name, descriptor); - } - return descriptor; - }; -} -/** - * InvocationExpr validation - */ -export default class FunctionInvocationValidator implements AstValidator { - validate(expr: InvocationExpr, accept: ValidationAcceptor): void { - const funcDecl = expr.function.ref; - if (!funcDecl) { - accept('error', 'function cannot be resolved', { node: expr }); - return; - } - - if (!this.validateArgs(funcDecl, expr.args, accept)) { - return; - } - - if (isFromStdlib(funcDecl)) { - // validate standard library functions - - // find the containing attribute context for the invocation - let curr: AstNode | undefined = expr.$container; - let containerAttribute: DataModelAttribute | DataModelFieldAttribute | undefined; - while (curr) { - if (isDataModelAttribute(curr) || isDataModelFieldAttribute(curr)) { - containerAttribute = curr; - break; - } - curr = curr.$container; - } - - // validate the context allowed for the function - const exprContext = this.getExpressionContext(containerAttribute); - - // get the context allowed for the function - const funcAllowedContext = getFunctionExpressionContext(funcDecl); - - if (funcAllowedContext.length > 0 && (!exprContext || !funcAllowedContext.includes(exprContext))) { - accept( - 'error', - `function "${funcDecl.name}" is not allowed in the current context${ - exprContext ? ': ' + exprContext : '' - }`, - { - node: expr, - } - ); - return; - } - - // TODO: express function validation rules declaratively in ZModel - - 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], - }); - } - } else if ( - funcAllowedContext.includes(ExpressionContext.AccessPolicy) || - funcAllowedContext.includes(ExpressionContext.ValidationRule) - ) { - // filter operation functions validation - - // first argument must refer to a model field - const firstArg = expr.args?.[0]?.value; - if (firstArg) { - if (!getFieldReference(firstArg)) { - accept('error', 'first argument must be a field reference', { node: firstArg }); - } - } - - // second argument must be a literal or array of literal - const secondArg = expr.args?.[1]?.value; - if ( - secondArg && - // literal - !isLiteralExpr(secondArg) && - // enum field - !isEnumFieldReference(secondArg) && - // `auth()...` expression - !isAuthOrAuthMemberAccess(secondArg) && - // static function calls that are runtime constants: `currentModel`, `currentOperation` - !this.isStaticFunctionCall(secondArg) && - // array of literal/enum - !( - isArrayExpr(secondArg) && - secondArg.items.every( - (item) => - isLiteralExpr(item) || isEnumFieldReference(item) || isAuthOrAuthMemberAccess(item) - ) - ) - ) { - accept( - 'error', - 'second argument must be a literal, an enum, an expression starting with `auth().`, or an array of them', - { - node: secondArg, - } - ); - } - } - } - - // run checkers for specific functions - const checker = invocationCheckers.get(expr.function.$refText); - if (checker) { - checker.value.call(this, expr, accept); - } - } - - private getExpressionContext(containerAttribute: DataModelAttribute | DataModelFieldAttribute | undefined) { - if (!containerAttribute) { - return undefined; - } - if (isValidationAttribute(containerAttribute)) { - return ExpressionContext.ValidationRule; - } - return match(containerAttribute?.decl.$refText) - .with('@default', () => ExpressionContext.DefaultValue) - .with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy) - .with('@@index', () => ExpressionContext.Index) - .otherwise(() => undefined); - } - - private isStaticFunctionCall(expr: Expression) { - return isInvocationExpr(expr) && ['currentModel', 'currentOperation'].includes(expr.function.$refText); - } - - 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]; - const arg = args[i]; - if (!arg) { - if (!param.optional) { - accept('error', `missing argument for parameter "${param.name}"`, { node: funcDecl }); - success = false; - } - } else { - if (!this.validateInvocationArg(arg, param, accept)) { - success = false; - } - } - } - // TODO: do we need to complain for extra arguments? - return success; - } - - private validateInvocationArg(arg: Argument, param: FunctionParam, accept: ValidationAcceptor) { - const argResolvedType = arg?.value?.$resolvedType; - if (!argResolvedType) { - accept('error', 'argument type cannot be resolved', { node: arg }); - return false; - } - - const dstType = param.type.type; - if (!dstType) { - accept('error', 'parameter type cannot be resolved', { node: param }); - return false; - } - - const dstIsArray = param.type.array; - const dstRef = param.type.reference; - - if (dstType === 'Any' && !dstIsArray) { - // scalar 'any' can be assigned with anything - return true; - } - - if (typeof argResolvedType.decl === 'string') { - // scalar type - if (!typeAssignable(dstType, argResolvedType.decl, arg.value) || dstIsArray !== argResolvedType.array) { - accept('error', `argument is not assignable to parameter`, { - node: arg, - }); - return false; - } - } else { - // enum or model type - if ((dstRef?.ref !== argResolvedType.decl && dstType !== 'Any') || dstIsArray !== argResolvedType.array) { - accept('error', `argument is not assignable to parameter`, { - node: arg, - }); - return false; - } - } - - return true; - } - - @func('check') - private _checkCheck(expr: InvocationExpr, accept: ValidationAcceptor) { - let valid = true; - - const fieldArg = expr.args[0].value; - if (!isDataModelFieldReference(fieldArg) || !isDataModel(fieldArg.$resolvedType?.decl)) { - accept('error', 'argument must be a relation field', { node: expr.args[0] }); - valid = false; - } - - if (fieldArg.$resolvedType?.array) { - accept('error', 'argument cannot be an array field', { node: expr.args[0] }); - valid = false; - } - - 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] }); - valid = false; - } - } - - if (!valid) { - return; - } - - // check for cyclic relation checking - const start = fieldArg.$resolvedType?.decl as DataModel; - const tasks = [expr]; - const seen = new Set(); - - while (tasks.length > 0) { - const currExpr = tasks.pop()!; - const arg = currExpr.args[0]?.value; - - if (!isDataModel(arg?.$resolvedType?.decl)) { - continue; - } - - const currModel = arg.$resolvedType.decl; - - if (seen.has(currModel)) { - if (currModel === start) { - 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 - } - break; - } else { - seen.add(currModel); - } - - const policyAttrs = currModel.attributes.filter( - (attr) => attr.decl.$refText === '@@allow' || attr.decl.$refText === '@@deny' - ); - for (const attr of policyAttrs) { - const rule = attr.args[1]; - if (!rule) { - continue; - } - streamAst(rule).forEach((node) => { - if (isCheckInvocation(node)) { - tasks.push(node as InvocationExpr); - } - }); - } - } - } -} diff --git a/packages/schema/src/language-server/validator/schema-validator.ts b/packages/schema/src/language-server/validator/schema-validator.ts deleted file mode 100644 index 0757c6993..000000000 --- a/packages/schema/src/language-server/validator/schema-validator.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Model, isDataModel, isDataSource } from '@zenstackhq/language/ast'; -import { getDataModelAndTypeDefs, hasAttribute } from '@zenstackhq/sdk'; -import { LangiumDocuments, ValidationAcceptor } from 'langium'; -import { getAllDeclarationsIncludingImports, resolveImport, resolveTransitiveImports } from '../../utils/ast-utils'; -import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../constants'; -import { AstValidator } from '../types'; -import { validateDuplicatedDeclarations } from './utils'; - -/** - * Validates toplevel schema. - */ -export default class SchemaValidator implements AstValidator { - constructor(protected readonly documents: LangiumDocuments) {} - validate(model: Model, accept: ValidationAcceptor): void { - this.validateImports(model, accept); - validateDuplicatedDeclarations(model, model.declarations, accept); - - const importedModels = resolveTransitiveImports(this.documents, model); - - 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', - }); - } - } - - if ( - !model.$document?.uri.path.endsWith(STD_LIB_MODULE_NAME) && - !model.$document?.uri.path.endsWith(PLUGIN_MODULE_NAME) - ) { - this.validateDataSources(model, accept); - } - - // at most one `@@auth` model - const decls = getDataModelAndTypeDefs(model, true); - const authModels = decls.filter((d) => isDataModel(d) && hasAttribute(d, '@@auth')); - if (authModels.length > 1) { - accept('error', 'Multiple `@@auth` models are not allowed', { node: authModels[1] }); - } - } - - private validateDataSources(model: Model, accept: ValidationAcceptor) { - const dataSources = getAllDeclarationsIncludingImports(this.documents, model).filter((d) => isDataSource(d)); - if (dataSources.length > 1) { - accept('error', 'Multiple datasource declarations are not allowed', { node: dataSources[1] }); - } - } - - 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/schema/src/language-server/validator/typedef-validator.ts b/packages/schema/src/language-server/validator/typedef-validator.ts deleted file mode 100644 index 70b6ec860..000000000 --- a/packages/schema/src/language-server/validator/typedef-validator.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { TypeDef, TypeDefField } from '@zenstackhq/language/ast'; -import { ValidationAcceptor } from 'langium'; -import { AstValidator } from '../types'; -import { validateAttributeApplication } from './attribute-application-validator'; -import { validateDuplicatedDeclarations } from './utils'; - -/** - * Validates type def declarations. - */ -export default class TypeDefValidator implements AstValidator { - validate(typeDef: TypeDef, accept: ValidationAcceptor): void { - validateDuplicatedDeclarations(typeDef, typeDef.fields, accept); - this.validateAttributes(typeDef, accept); - this.validateFields(typeDef, accept); - } - - private validateAttributes(typeDef: TypeDef, accept: ValidationAcceptor) { - 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)); - } -} diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts deleted file mode 100644 index 032bf9a5e..000000000 --- a/packages/schema/src/language-server/validator/utils.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - BuiltinType, - Expression, - ExpressionType, - isDataModelField, - isMemberAccessExpr, - isStringLiteral, -} from '@zenstackhq/language/ast'; -import { isAuthInvocation } from '@zenstackhq/sdk'; -import { AstNode, ValidationAcceptor } from 'langium'; - -/** - * Checks if the given declarations have duplicated names - */ -export function validateDuplicatedDeclarations( - container: AstNode, - decls: Array, - accept: ValidationAcceptor -): void { - const groupByName = decls.reduce>>((group, decl) => { - group[decl.name] = group[decl.name] ?? []; - group[decl.name].push(decl); - return group; - }, {}); - - for (const [name, decls] of Object.entries(groupByName)) { - if (decls.length > 1) { - let errorField = decls[1]; - if (isDataModelField(decls[0])) { - const nonInheritedFields = decls.filter((x) => !(isDataModelField(x) && x.$container !== container)); - if (nonInheritedFields.length > 0) { - errorField = nonInheritedFields.slice(-1)[0]; - } - } - - accept('error', `Duplicated declaration name "${name}"`, { - node: errorField, - }); - } - } -} - -/** - * Try getting string value from a potential string literal expression - */ -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; - -/** - * Determines if the given sourceType is assignable to a destination of destType - */ -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)) { - const literal = getStringLiteral(sourceExpr); - if (literal && isoDateTimeRegex.test(literal)) { - // implicitly convert to DateTime - sourceType = 'DateTime'; - } - } - - switch (destType) { - case 'Any': - return true; - case 'Float': - return sourceType === 'Any' || sourceType === 'Int' || sourceType === 'Float'; - default: - return sourceType === 'Any' || sourceType === destType; - } -} - -/** - * Maps a ZModel builtin type to expression type - */ -export function mapBuiltinTypeToExpressionType( - type: BuiltinType | 'Any' | 'Object' | 'Null' | 'Unsupported' -): ExpressionType | 'Any' { - switch (type) { - case 'Any': - case 'Boolean': - case 'String': - case 'DateTime': - case 'Int': - case 'Float': - case 'Null': - return type; - case 'BigInt': - return 'Int'; - case 'Decimal': - return 'Float'; - case 'Json': - case 'Bytes': - return 'Any'; - case 'Object': - return 'Object'; - case 'Unsupported': - return 'Unsupported'; - } -} - -export function isAuthOrAuthMemberAccess(expr: Expression): boolean { - return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand)); -} diff --git a/packages/schema/src/language-server/validator/zmodel-validator.ts b/packages/schema/src/language-server/validator/zmodel-validator.ts deleted file mode 100644 index c1dcbb09e..000000000 --- a/packages/schema/src/language-server/validator/zmodel-validator.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - Attribute, - DataModel, - DataSource, - Enum, - Expression, - FunctionDecl, - InvocationExpr, - Model, - TypeDef, - ZModelAstType, -} from '@zenstackhq/language/ast'; -import { AstNode, LangiumDocument, ValidationAcceptor, ValidationChecks, ValidationRegistry } from 'langium'; -import type { ZModelServices } from '../zmodel-module'; -import AttributeValidator from './attribute-validator'; -import DataModelValidator from './datamodel-validator'; -import DataSourceValidator from './datasource-validator'; -import EnumValidator from './enum-validator'; -import ExpressionValidator from './expression-validator'; -import FunctionDeclValidator from './function-decl-validator'; -import FunctionInvocationValidator from './function-invocation-validator'; -import SchemaValidator from './schema-validator'; -import TypeDefValidator from './typedef-validator'; - -/** - * Registry for validation checks. - */ -export class ZModelValidationRegistry extends ValidationRegistry { - constructor(services: ZModelServices) { - super(services); - const validator = services.validation.ZModelValidator; - const checks: ValidationChecks = { - Model: validator.checkModel, - DataSource: validator.checkDataSource, - DataModel: validator.checkDataModel, - TypeDef: validator.checkTypeDef, - Enum: validator.checkEnum, - Attribute: validator.checkAttribute, - Expression: validator.checkExpression, - InvocationExpr: validator.checkFunctionInvocation, - FunctionDecl: validator.checkFunctionDecl, - }; - this.register(checks, validator); - } -} - -/** - * Implementation of custom validations. - */ -export class ZModelValidator { - constructor(protected readonly services: ZModelServices) {} - private shouldCheck(node: AstNode) { - let doc: LangiumDocument | undefined; - let currNode: AstNode | undefined = node; - while (currNode) { - if (currNode.$document) { - doc = currNode.$document; - break; - } - currNode = currNode.$container; - } - - 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); - } - - checkDataSource(node: DataSource, accept: ValidationAcceptor): void { - this.shouldCheck(node) && new DataSourceValidator().validate(node, accept); - } - - checkDataModel(node: DataModel, accept: ValidationAcceptor): void { - this.shouldCheck(node) && new DataModelValidator().validate(node, accept); - } - - checkTypeDef(node: TypeDef, accept: ValidationAcceptor): void { - this.shouldCheck(node) && new TypeDefValidator().validate(node, accept); - } - - checkEnum(node: Enum, accept: ValidationAcceptor): void { - this.shouldCheck(node) && new EnumValidator().validate(node, accept); - } - - checkAttribute(node: Attribute, accept: ValidationAcceptor): void { - this.shouldCheck(node) && new AttributeValidator().validate(node, accept); - } - - checkExpression(node: Expression, accept: ValidationAcceptor): void { - this.shouldCheck(node) && new ExpressionValidator().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); - } -} diff --git a/packages/schema/src/language-server/zmodel-code-action.ts b/packages/schema/src/language-server/zmodel-code-action.ts deleted file mode 100644 index a4c99da96..000000000 --- a/packages/schema/src/language-server/zmodel-code-action.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { DataModel, DataModelField, Model, isDataModel } from '@zenstackhq/language/ast'; -import { - AstReflection, - CodeActionProvider, - IndexManager, - LangiumDocument, - LangiumDocuments, - LangiumServices, - MaybePromise, - getDocument, -} from 'langium'; - -import { getModelFieldsWithBases } from '@zenstackhq/sdk'; -import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver'; -import { IssueCodes } from './constants'; -import { MissingOppositeRelationData } from './validator/datamodel-validator'; -import { ZModelFormatter } from './zmodel-formatter'; - -export class ZModelCodeActionProvider implements CodeActionProvider { - protected readonly reflection: AstReflection; - protected readonly indexManager: IndexManager; - protected readonly formatter: ZModelFormatter; - protected readonly documents: LangiumDocuments; - - constructor(services: LangiumServices) { - this.reflection = services.shared.AstReflection; - this.indexManager = services.shared.workspace.IndexManager; - this.formatter = services.lsp.Formatter as ZModelFormatter; - this.documents = services.shared.workspace.LangiumDocuments; - } - - getCodeActions( - document: LangiumDocument, - params: CodeActionParams - ): MaybePromise | undefined> { - const result: CodeAction[] = []; - const acceptor = (ca: CodeAction | undefined) => ca && result.push(ca); - for (const diagnostic of params.context.diagnostics) { - this.createCodeActions(diagnostic, document, acceptor); - } - return result; - } - - private createCodeActions( - diagnostic: Diagnostic, - document: LangiumDocument, - accept: (ca: CodeAction | undefined) => void - ) { - switch (diagnostic.code) { - case IssueCodes.MissingOppositeRelation: - accept(this.fixMissingOppositeRelation(diagnostic, document)); - } - - return undefined; - } - - private fixMissingOppositeRelation(diagnostic: Diagnostic, document: LangiumDocument): CodeAction | undefined { - const data = diagnostic.data as MissingOppositeRelationData; - - const rootCst = - data.relationFieldDocUri == document.textDocument.uri - ? document.parseResult.value - : this.documents.all.find((doc) => doc.textDocument.uri === data.relationFieldDocUri)?.parseResult - .value; - - if (rootCst) { - const fieldModel = rootCst as Model; - const fieldAstNode = ( - fieldModel.declarations.find( - (x) => isDataModel(x) && x.name === data.relationDataModelName - ) as DataModel - )?.fields.find((x) => x.name === data.relationFieldName) as DataModelField; - - if (!fieldAstNode) return undefined; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const oppositeModel = fieldAstNode.type.reference!.ref! as DataModel; - - const currentModel = document.parseResult.value as Model; - - const container = currentModel.declarations.find( - (decl) => decl.name === data.dataModelName && isDataModel(decl) - ) as DataModel; - - if (container && container.$cstNode) { - // indent - let indent = '\t'; - const formatOptions = this.formatter.getFormatOptions(); - if (formatOptions?.insertSpaces) { - indent = ' '.repeat(formatOptions.tabSize); - } - indent = indent.repeat(this.formatter.getIndent()); - - let newText = ''; - if (fieldAstNode.type.array) { - // post Post[] - const idField = getModelFieldsWithBases(container).find((f) => - f.attributes.find((attr) => attr.decl.ref?.name === '@id') - ) as DataModelField; - - // if no id field, we can't generate reference - if (!idField) { - return undefined; - } - - const typeName = container.name; - const fieldName = this.lowerCaseFirstLetter(typeName); - - // might already exist - let referenceField = ''; - - const idFieldName = idField.name; - const referenceIdFieldName = fieldName + this.upperCaseFirstLetter(idFieldName); - - if (!getModelFieldsWithBases(oppositeModel).find((f) => f.name === referenceIdFieldName)) { - referenceField = '\n' + indent + `${referenceIdFieldName} ${idField.type.type}`; - } - - newText = - '\n' + - indent + - `${fieldName} ${typeName} @relation(fields: [${referenceIdFieldName}], references: [${idFieldName}])` + - referenceField + - '\n'; - } else { - // user User @relation(fields: [userAbc], references: [id]) - const typeName = container.name; - const fieldName = this.lowerCaseFirstLetter(typeName); - newText = '\n' + indent + `${fieldName} ${typeName}[]` + '\n'; - } - - // the opposite model might be in the imported file - const targetDocument = getDocument(oppositeModel); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const endOffset = oppositeModel.$cstNode!.end - 1; - const position = targetDocument.textDocument.positionAt(endOffset); - - return { - title: `Add opposite relation fields on ${oppositeModel.name}`, - kind: CodeActionKind.QuickFix, - diagnostics: [diagnostic], - isPreferred: false, - edit: { - changes: { - [targetDocument.textDocument.uri]: [ - { - range: { - start: position, - end: position, - }, - newText, - }, - ], - }, - }, - }; - } - } - - return undefined; - } - - private lowerCaseFirstLetter(str: string) { - return str.charAt(0).toLowerCase() + str.slice(1); - } - - private upperCaseFirstLetter(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); - } -} diff --git a/packages/schema/src/language-server/zmodel-completion-provider.ts b/packages/schema/src/language-server/zmodel-completion-provider.ts deleted file mode 100644 index cd6dae0ca..000000000 --- a/packages/schema/src/language-server/zmodel-completion-provider.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { - DataModelAttribute, - DataModelFieldAttribute, - ReferenceExpr, - StringLiteral, - isArrayExpr, - isAttribute, - isDataModel, - isDataModelAttribute, - isDataModelField, - isDataModelFieldAttribute, - isEnum, - isEnumField, - isFunctionDecl, - isInvocationExpr, - isMemberAccessExpr, -} from '@zenstackhq/language/ast'; -import { ZModelCodeGenerator, getAttribute, isEnumFieldReference, isFromStdlib } from '@zenstackhq/sdk'; -import { - AstNode, - AstNodeDescription, - CompletionAcceptor, - CompletionContext, - CompletionProviderOptions, - CompletionValueItem, - DefaultCompletionProvider, - LangiumDocument, - LangiumServices, - MaybePromise, - NextFeature, -} from 'langium'; -import { P, match } from 'ts-pattern'; -import { CompletionItemKind, CompletionList, CompletionParams, MarkupContent } from 'vscode-languageserver'; - -export class ZModelCompletionProvider extends DefaultCompletionProvider { - constructor(private readonly services: LangiumServices) { - super(services); - } - - readonly completionOptions?: CompletionProviderOptions = { - triggerCharacters: ['@', '(', ',', '.'], - }; - - override async getCompletion( - document: LangiumDocument, - params: CompletionParams - ): Promise { - try { - return await super.getCompletion(document, params); - } catch (e) { - console.error('Completion error:', (e as Error).message); - return undefined; - } - } - - override completionFor( - context: CompletionContext, - next: NextFeature, - acceptor: CompletionAcceptor - ): MaybePromise { - if (isDataModelAttribute(context.node) || isDataModelFieldAttribute(context.node)) { - const completions = this.getCompletionFromHint(context.node); - if (completions) { - completions.forEach((c) => acceptor(context, c)); - return; - } - } - return super.completionFor(context, next, acceptor); - } - - private getCompletionFromHint( - contextNode: DataModelAttribute | DataModelFieldAttribute - ): CompletionValueItem[] | undefined { - // get completion based on the hint on the next unfilled parameter - const unfilledParams = this.getUnfilledAttributeParams(contextNode); - const nextParam = unfilledParams[0]; - if (!nextParam) { - return undefined; - } - - const hintAttr = getAttribute(nextParam, '@@@completionHint'); - if (hintAttr) { - const hint = hintAttr.args[0]; - if (hint?.value) { - if (isArrayExpr(hint.value)) { - return hint.value.items.map((item) => { - return { - label: `${(item as StringLiteral).value}`, - kind: CompletionItemKind.Value, - detail: 'Parameter', - sortText: '0', - }; - }); - } - } - } - return undefined; - } - - // TODO: this doesn't work when the file contains parse errors - private getUnfilledAttributeParams(contextNode: DataModelAttribute | DataModelFieldAttribute) { - try { - const params = contextNode.decl.ref?.params; - if (params) { - const args = contextNode.args; - let unfilledParams = [...params]; - args.forEach((arg) => { - if (arg.name) { - unfilledParams = unfilledParams.filter((p) => p.name !== arg.name); - } else { - unfilledParams.shift(); - } - }); - - return unfilledParams; - } - } catch { - // noop - } - return []; - } - - override completionForCrossReference( - context: CompletionContext, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - crossRef: any, - acceptor: CompletionAcceptor - ): MaybePromise { - if (crossRef.property === 'member' && !isMemberAccessExpr(context.node)) { - // for guarding an error in the base implementation - return; - } - - const customAcceptor = (context: CompletionContext, item: CompletionValueItem) => { - // attributes starting with @@@ are for internal use only - if (item.insertText?.startsWith('@@@') || item.label?.startsWith('@@@')) { - return; - } - - if ('nodeDescription' in item) { - const node = this.getAstNode(item.nodeDescription); - if (!node) { - return; - } - - // enums in stdlib are not supposed to be referenced directly - if ((isEnum(node) || isEnumField(node)) && isFromStdlib(node)) { - return; - } - - if ( - (isDataModelAttribute(context.node) || isDataModelFieldAttribute(context.node)) && - !this.filterAttributeApplicationCompletion(context.node, node) - ) { - // node not matching attribute context - return; - } - } - acceptor(context, item); - }; - - return super.completionForCrossReference(context, crossRef, customAcceptor); - } - - override completionForKeyword( - context: CompletionContext, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - keyword: any, - acceptor: CompletionAcceptor - ): MaybePromise { - const customAcceptor = (context: CompletionContext, item: CompletionValueItem) => { - if (!this.filterKeywordForContext(context, keyword.value)) { - return; - } - acceptor(context, item); - }; - return super.completionForKeyword(context, keyword, customAcceptor); - } - - private filterKeywordForContext(context: CompletionContext, keyword: string) { - if (isInvocationExpr(context.node)) { - return ['true', 'false', 'null', 'this'].includes(keyword); - } else if (isDataModelAttribute(context.node) || isDataModelFieldAttribute(context.node)) { - const exprContext = this.getAttributeContextType(context.node); - if (exprContext === 'DefaultValue') { - return ['true', 'false', 'null'].includes(keyword); - } else { - return ['true', 'false', 'null', 'this'].includes(keyword); - } - } else { - return true; - } - } - - private filterAttributeApplicationCompletion( - contextNode: DataModelAttribute | DataModelFieldAttribute, - node: AstNode - ) { - const attrContextType = this.getAttributeContextType(contextNode); - - if (isFunctionDecl(node) && attrContextType) { - // functions are excluded if they are not allowed in the current context - const funcExprContextAttr = getAttribute(node, '@@@expressionContext'); - if (funcExprContextAttr && funcExprContextAttr.args[0]) { - const arg = funcExprContextAttr.args[0]; - if (isArrayExpr(arg.value)) { - return arg.value.items.some( - (item) => - isEnumFieldReference(item) && (item as ReferenceExpr).target.$refText === attrContextType - ); - } - } - return false; - } - - if (isDataModelField(node)) { - // model fields are not allowed in @default - return attrContextType !== 'DefaultValue'; - } - - return true; - } - - private getAttributeContextType(node: DataModelAttribute | DataModelFieldAttribute) { - return match(node.decl.$refText) - .with('@default', () => 'DefaultValue') - .with(P.union('@@allow', '@allow', '@@deny', '@deny'), () => 'AccessPolicy') - .with('@@validate', () => 'ValidationRule') - .otherwise(() => undefined); - } - - override createReferenceCompletionItem(nodeDescription: AstNodeDescription): CompletionValueItem { - const node = this.getAstNode(nodeDescription); - const documentation = this.getNodeDocumentation(node); - - return match(node) - .when(isDataModel, () => ({ - nodeDescription, - kind: CompletionItemKind.Class, - detail: 'Data model', - sortText: '1', - documentation, - })) - .when(isDataModelField, () => ({ - nodeDescription, - kind: CompletionItemKind.Field, - detail: 'Data model field', - sortText: '0', - documentation, - })) - .when(isEnum, () => ({ - nodeDescription, - kind: CompletionItemKind.Class, - detail: 'Enum', - sortText: '1', - documentation, - })) - .when(isEnumField, () => ({ - nodeDescription, - kind: CompletionItemKind.Enum, - detail: 'Enum value', - sortText: '1', - documentation, - })) - .when(isFunctionDecl, () => ({ - nodeDescription, - insertText: this.getFunctionInsertText(nodeDescription), - kind: CompletionItemKind.Function, - detail: 'Function', - sortText: '1', - documentation, - })) - .when(isAttribute, () => ({ - nodeDescription, - insertText: this.getAttributeInsertText(nodeDescription), - kind: CompletionItemKind.Property, - detail: 'Attribute', - sortText: '1', - documentation, - })) - .otherwise(() => ({ - nodeDescription, - kind: CompletionItemKind.Reference, - detail: nodeDescription.type, - sortText: '2', - documentation, - })); - } - - private getFunctionInsertText(nodeDescription: AstNodeDescription): string { - const node = this.getAstNode(nodeDescription); - if (isFunctionDecl(node)) { - if (node.params.some((p) => !p.optional)) { - return nodeDescription.name; - } - } - return `${nodeDescription.name}()`; - } - - private getAttributeInsertText(nodeDescription: AstNodeDescription): string { - const node = this.getAstNode(nodeDescription); - if (isAttribute(node)) { - if (node.name === '@relation') { - return `${nodeDescription.name}(fields: [], references: [])`; - } - } - return nodeDescription.name; - } - - private getAstNode(nodeDescription: AstNodeDescription) { - let node = nodeDescription.node; - if (!node) { - const doc = this.services.shared.workspace.LangiumDocuments.getOrCreateDocument( - nodeDescription.documentUri - ); - if (!doc) { - return undefined; - } - node = this.services.workspace.AstNodeLocator.getAstNode(doc.parseResult.value, nodeDescription.path); - if (!node) { - return undefined; - } - } - return node; - } - - private getNodeDocumentation(node?: AstNode): MarkupContent | undefined { - if (!node) { - return undefined; - } - const md = this.commentsToMarkdown(node); - return { - kind: 'markdown', - value: md, - }; - } - - private commentsToMarkdown(node: AstNode): string { - const md = this.services.documentation.DocumentationProvider.getDocumentation(node) ?? ''; - const zModelGenerator = new ZModelCodeGenerator(); - const docs: string[] = []; - - try { - match(node) - .when(isAttribute, (attr) => { - const zModelGenerator = new ZModelCodeGenerator(); - docs.push('```prisma', zModelGenerator.generate(attr), '```'); - }) - .when(isFunctionDecl, (func) => { - docs.push('```ts', zModelGenerator.generate(func), '```'); - }) - .when(isDataModel, (model) => { - docs.push('```prisma', `model ${model.name} { ... }`, '```'); - }) - .when(isEnum, (enumDecl) => { - docs.push('```prisma', zModelGenerator.generate(enumDecl), '```'); - }) - .when(isDataModelField, (field) => { - docs.push(`${field.name}: ${field.type.type ?? field.type.reference?.$refText}`); - }) - .otherwise((ast) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const name = (ast as any).name; - if (name) { - docs.push(name); - } - }); - } catch { - // noop - } - - if (md) { - docs.push('___', md); - } - return docs.join('\n'); - } -} diff --git a/packages/schema/src/language-server/zmodel-definition.ts b/packages/schema/src/language-server/zmodel-definition.ts deleted file mode 100644 index f4bb4a39b..000000000 --- a/packages/schema/src/language-server/zmodel-definition.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { DefaultDefinitionProvider, LangiumDocuments, LangiumServices, LeafCstNode, MaybePromise } from 'langium'; -import { DefinitionParams, LocationLink, Range } from 'vscode-languageserver'; -import { resolveImport } from '../utils/ast-utils'; -import { isModelImport } from '@zenstackhq/language/ast'; - -export class ZModelDefinitionProvider extends DefaultDefinitionProvider { - protected documents: LangiumDocuments; - - constructor(services: LangiumServices) { - super(services); - this.documents = services.shared.workspace.LangiumDocuments; - } - protected override collectLocationLinks( - sourceCstNode: LeafCstNode, - _params: DefinitionParams - ): MaybePromise { - if (isModelImport(sourceCstNode.element)) { - const importedModel = resolveImport(this.documents, sourceCstNode.element); - if (importedModel?.$document) { - const targetObject = importedModel; - const selectionRange = this.nameProvider.getNameNode(targetObject)?.range ?? Range.create(0, 0, 0, 0); - const previewRange = targetObject.$cstNode?.range ?? Range.create(0, 0, 0, 0); - return [ - LocationLink.create( - importedModel.$document.uri.toString(), - previewRange, - selectionRange, - sourceCstNode.range - ), - ]; - } - return undefined; - } - return super.collectLocationLinks(sourceCstNode, _params); - } -} diff --git a/packages/schema/src/language-server/zmodel-documentation-provider.ts b/packages/schema/src/language-server/zmodel-documentation-provider.ts deleted file mode 100644 index f960507bc..000000000 --- a/packages/schema/src/language-server/zmodel-documentation-provider.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AstNode, JSDocDocumentationProvider } from 'langium'; - -/** - * Documentation provider that first tries to use triple-slash comments and falls back to JSDoc comments. - */ -export class ZModelDocumentationProvider extends JSDocDocumentationProvider { - getDocumentation(node: AstNode): string | undefined { - // prefer to use triple-slash comments - if ('comments' in node && Array.isArray(node.comments) && node.comments.length > 0) { - return node.comments.map((c: string) => c.replace(/^[/]*\s*/, '')).join('\n'); - } - - // fall back to JSDoc comments - return super.getDocumentation(node); - } -} diff --git a/packages/schema/src/language-server/zmodel-formatter.ts b/packages/schema/src/language-server/zmodel-formatter.ts deleted file mode 100644 index 92296176a..000000000 --- a/packages/schema/src/language-server/zmodel-formatter.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - AbstractFormatter, - AstNode, - ConfigurationProvider, - Formatting, - LangiumDocument, - LangiumServices, - MaybePromise, -} from 'langium'; - -import * as ast from '@zenstackhq/language/ast'; -import { DocumentFormattingParams, FormattingOptions, TextEdit } from 'vscode-languageserver'; -import { ZModelLanguageMetaData } from '@zenstackhq/language/generated/module'; - -export class ZModelFormatter extends AbstractFormatter { - private formatOptions?: FormattingOptions; - private isPrismaStyle = true; - - protected readonly configurationProvider: ConfigurationProvider; - - constructor(services: LangiumServices) { - super(); - this.configurationProvider = services.shared.workspace.ConfigurationProvider; - } - - protected format(node: AstNode): void { - const formatter = this.getNodeFormatter(node); - - if (ast.isDataModelField(node) || ast.isTypeDefField(node)) { - if (this.isPrismaStyle && (ast.isDataModel(node.$container) || ast.isTypeDef(node.$container))) { - const dataModel = node.$container; - - const compareFn = (a: number, b: number) => b - a; - const maxNameLength = dataModel.fields.map((x) => x.name.length).sort(compareFn)[0]; - const maxTypeLength = dataModel.fields.map(this.getFieldTypeLength).sort(compareFn)[0]; - - formatter.property('type').prepend(Formatting.spaces(maxNameLength - node.name.length + 1)); - if (node.attributes.length > 0) { - formatter - .node(node.attributes[0]) - .prepend(Formatting.spaces(maxTypeLength - this.getFieldTypeLength(node) + 1)); - - formatter.nodes(...node.attributes.slice(1)).prepend(Formatting.oneSpace()); - } - } else { - formatter.property('type').prepend(Formatting.oneSpace()); - if (node.attributes.length > 0) { - formatter.properties('attributes').prepend(Formatting.oneSpace()); - } - } - } else if (ast.isDataModelFieldAttribute(node)) { - formatter.keyword('(').surround(Formatting.noSpace()); - formatter.keyword(')').prepend(Formatting.noSpace()); - formatter.keyword(',').append(Formatting.oneSpace()); - if (node.args.length > 1) { - formatter.nodes(...node.args.slice(1)).prepend(Formatting.oneSpace()); - } - } else if (ast.isAttributeArg(node)) { - formatter.keyword(':').prepend(Formatting.noSpace()); - formatter.keyword(':').append(Formatting.oneSpace()); - } else if (ast.isAbstractDeclaration(node)) { - const bracesOpen = formatter.keyword('{'); - const bracesClose = formatter.keyword('}'); - // allow extra blank lines between declarations - formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent({ allowMore: true })); - bracesOpen.prepend(Formatting.oneSpace()); - bracesClose.prepend(Formatting.newLine()); - } else if (ast.isModel(node)) { - const model = node as ast.Model; - const nodes = formatter.nodes(...model.declarations); - nodes.prepend(Formatting.noIndent()); - } - } - - override formatDocument( - document: LangiumDocument, - params: DocumentFormattingParams - ): MaybePromise { - this.formatOptions = params.options; - - this.configurationProvider.getConfiguration(ZModelLanguageMetaData.languageId, 'format').then((config) => { - // in the CLI case, the config is undefined - if (config) { - if (config.usePrismaStyle === false) { - this.setPrismaStyle(false); - } else { - this.setPrismaStyle(true); - } - } - }); - - return super.formatDocument(document, params); - } - - public getFormatOptions(): FormattingOptions | undefined { - return this.formatOptions; - } - - public getIndent() { - return 1; - } - - public setPrismaStyle(isPrismaStyle: boolean) { - this.isPrismaStyle = isPrismaStyle; - } - - private getFieldTypeLength(field: ast.DataModelField | ast.TypeDefField) { - let length: number; - - if (field.type.type) { - length = field.type.type.length; - } else if (field.type.reference) { - length = field.type.reference.$refText.length; - } else if (ast.isDataModelField(field) && field.type.unsupported) { - const name = `Unsupported("${field.type.unsupported.value.value}")`; - length = name.length; - } else { - // we shouldn't get here - length = 1; - } - - if (field.type.optional) { - length += 1; - } - - if (field.type.array) { - length += 2; - } - - return length; - } -} diff --git a/packages/schema/src/language-server/zmodel-highlight.ts b/packages/schema/src/language-server/zmodel-highlight.ts deleted file mode 100644 index dc8fcc62e..000000000 --- a/packages/schema/src/language-server/zmodel-highlight.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { DefaultDocumentHighlightProvider, LangiumDocument } from 'langium'; -import { DocumentHighlight, DocumentHighlightParams } from 'vscode-languageserver'; - -export class ZModelHighlightProvider extends DefaultDocumentHighlightProvider { - override async getDocumentHighlight( - document: LangiumDocument, - params: DocumentHighlightParams - ): Promise { - try { - return await super.getDocumentHighlight(document, params); - } catch (e) { - console.error('Highlight error:', (e as Error).message); - return undefined; - } - } -} diff --git a/packages/schema/src/language-server/zmodel-hover.ts b/packages/schema/src/language-server/zmodel-hover.ts deleted file mode 100644 index 06c40caaf..000000000 --- a/packages/schema/src/language-server/zmodel-hover.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AstNode, LangiumDocument, MultilineCommentHoverProvider } from 'langium'; -import { Hover, HoverParams } from 'vscode-languageclient'; - -export class ZModelHoverProvider extends MultilineCommentHoverProvider { - override async getHoverContent( - document: LangiumDocument, - params: HoverParams - ): Promise { - try { - return await super.getHoverContent(document, params); - } catch (e) { - console.error('Hover error:', (e as Error).message); - return undefined; - } - } -} diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts deleted file mode 100644 index 1e2491bda..000000000 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ /dev/null @@ -1,548 +0,0 @@ -import { - ArrayExpr, - AttributeArg, - AttributeParam, - BinaryExpr, - BooleanLiteral, - DataModel, - DataModelField, - DataModelFieldType, - Enum, - EnumField, - ExpressionType, - FunctionDecl, - FunctionParam, - FunctionParamType, - InvocationExpr, - LiteralExpr, - MemberAccessExpr, - NullExpr, - NumberLiteral, - ObjectExpr, - ReferenceExpr, - ReferenceTarget, - ResolvedShape, - StringLiteral, - ThisExpr, - TypeDefFieldType, - UnaryExpr, - isArrayExpr, - isBooleanLiteral, - isDataModel, - isDataModelField, - isDataModelFieldType, - isEnum, - isNumberLiteral, - isReferenceExpr, - isStringLiteral, - isTypeDefField, -} from '@zenstackhq/language/ast'; -import { getAuthDecl, getModelFieldsWithBases, isAuthInvocation, isFutureExpr } from '@zenstackhq/sdk'; -import { - AstNode, - AstNodeDescription, - AstNodeDescriptionProvider, - DefaultLinker, - DocumentState, - LangiumDocument, - LangiumServices, - LinkingError, - Reference, - getContainerOfType, - interruptAndCheck, - isReference, - streamContents, -} from 'langium'; -import { match } from 'ts-pattern'; -import { CancellationToken } from 'vscode-jsonrpc'; -import { getAllLoadedAndReachableDataModelsAndTypeDefs, getContainingDataModel } from '../utils/ast-utils'; -import { isMemberContainer } from './utils'; -import { mapBuiltinTypeToExpressionType } from './validator/utils'; - -interface DefaultReference extends Reference { - _ref?: AstNode | LinkingError; - _nodeDescription?: AstNodeDescription; -} - -type ScopeProvider = (name: string) => ReferenceTarget | DataModel | undefined; - -/** - * Langium linker implementation which links references and resolves expression types - */ -export class ZModelLinker extends DefaultLinker { - private readonly descriptions: AstNodeDescriptionProvider; - - constructor(services: LangiumServices) { - super(services); - this.descriptions = services.workspace.AstNodeDescriptionProvider; - } - - //#region Reference linking - - async link(document: LangiumDocument, cancelToken = CancellationToken.None): Promise { - if (document.parseResult.lexerErrors?.length > 0 || document.parseResult.parserErrors?.length > 0) { - return; - } - - for (const node of streamContents(document.parseResult.value)) { - await interruptAndCheck(cancelToken); - this.resolve(node, document); - } - document.state = DocumentState.Linked; - } - - private linkReference( - container: AstNode, - property: string, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { - 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); - } - - //#endregion - - //#region Expression type resolving - - private resolveFromScopeProviders( - node: AstNode, - property: string, - document: LangiumDocument, - 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); - - // Add the reference to the document's array of references - document.references.push(reference); - - return target; - } - } - return null; - } - - private resolve(node: AstNode, document: LangiumDocument, extraScopes: ScopeProvider[] = []) { - switch (node.$type) { - case StringLiteral: - case NumberLiteral: - case BooleanLiteral: - this.resolveLiteral(node as LiteralExpr); - break; - - case InvocationExpr: - this.resolveInvocation(node as InvocationExpr, document, extraScopes); - break; - - case ArrayExpr: - this.resolveArray(node as ArrayExpr, document, extraScopes); - break; - - case ReferenceExpr: - this.resolveReference(node as ReferenceExpr, document, extraScopes); - break; - - case MemberAccessExpr: - this.resolveMemberAccess(node as MemberAccessExpr, document, extraScopes); - break; - - case UnaryExpr: - this.resolveUnary(node as UnaryExpr, document, extraScopes); - break; - - case BinaryExpr: - this.resolveBinary(node as BinaryExpr, document, extraScopes); - break; - - case ObjectExpr: - this.resolveObject(node as ObjectExpr, document, extraScopes); - break; - - case ThisExpr: - this.resolveThis(node as ThisExpr, document, extraScopes); - break; - - case NullExpr: - this.resolveNull(node as NullExpr, document, extraScopes); - break; - - case AttributeArg: - this.resolveAttributeArg(node as AttributeArg, document, extraScopes); - break; - - case DataModel: - this.resolveDataModel(node as DataModel, document, extraScopes); - break; - - case DataModelField: - this.resolveDataModelField(node as DataModelField, document, extraScopes); - break; - - default: - this.resolveDefault(node, document, extraScopes); - break; - } - } - - private resolveBinary(node: BinaryExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { - switch (node.operator) { - // TODO: support arithmetics? - // case '+': - // case '-': - // case '*': - // case '/': - // this.resolve(node.left, document, extraScopes); - // this.resolve(node.right, document, extraScopes); - // this.resolveToBuiltinTypeOrDecl(node, 'Int'); - // break; - - case '>': - case '>=': - case '<': - case '<=': - case '==': - case '!=': - case '&&': - case '||': - case 'in': - this.resolve(node.left, document, extraScopes); - this.resolve(node.right, document, extraScopes); - this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); - break; - - case '?': - case '!': - case '^': - this.resolveCollectionPredicate(node, document, extraScopes); - break; - - default: - throw Error(`Unsupported binary operator: ${node.operator}`); - } - } - - private resolveUnary(node: UnaryExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { - this.resolve(node.operand, document, extraScopes); - switch (node.operator) { - case '!': - this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); - break; - default: - throw Error(`Unsupported unary operator: ${node.operator}`); - } - } - - 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[]) { - this.resolveDefault(node, document, extraScopes); - - if (node.target.ref) { - // resolve type - if (node.target.ref.$type === EnumField) { - this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container); - } else { - this.resolveToDeclaredType(node, (node.target.ref as DataModelField | FunctionParam).type); - } - } - } - - private resolveArray(node: ArrayExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { - node.items.forEach((item) => this.resolve(item, document, extraScopes)); - - if (node.items.length > 0) { - const itemType = node.items[0].$resolvedType; - if (itemType?.decl) { - this.resolveToBuiltinTypeOrDecl(node, itemType.decl, true); - } - } else { - this.resolveToBuiltinTypeOrDecl(node, 'Any', true); - } - } - - 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)) { - // auth() function is resolved against all loaded and reachable documents - - // get all data models from loaded and reachable documents - const allDecls = getAllLoadedAndReachableDataModelsAndTypeDefs( - this.langiumDocuments(), - getContainerOfType(node, isDataModel) - ); - - const authDecl = getAuthDecl(allDecls); - if (authDecl) { - node.$resolvedType = { decl: authDecl, nullable: true }; - } - } else if (isFutureExpr(node)) { - // future() function is resolved to current model - node.$resolvedType = { decl: getContainingDataModel(node) }; - } else { - this.resolveToDeclaredType(node, funcDecl.returnType); - } - } - } - - private resolveLiteral(node: LiteralExpr) { - const type = match(node) - .when(isStringLiteral, () => 'String') - .when(isBooleanLiteral, () => 'Boolean') - .when(isNumberLiteral, () => 'Int') - .exhaustive(); - - if (type) { - this.resolveToBuiltinTypeOrDecl(node, type); - } - } - - private resolveMemberAccess( - node: MemberAccessExpr, - document: LangiumDocument, - extraScopes: ScopeProvider[] - ) { - this.resolveDefault(node, document, extraScopes); - const operandResolved = node.operand.$resolvedType; - - 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); - - if (node.$resolvedType && isAuthInvocation(node.operand)) { - // member access on auth() function is nullable - // because user may not have provided all fields - node.$resolvedType.nullable = true; - } - } - } - } - - 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) { - this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); - } else { - // error is reported in validation pass - } - } - - private resolveThis(node: ThisExpr, _document: LangiumDocument, extraScopes: ScopeProvider[]) { - // resolve from scopes first - for (const scope of extraScopes) { - const r = scope('this'); - if (isDataModel(r)) { - this.resolveToBuiltinTypeOrDecl(node, r); - return; - } - } - - let decl: AstNode | undefined = node.$container; - - while (decl && !isDataModel(decl)) { - decl = decl.$container; - } - - if (decl) { - this.resolveToBuiltinTypeOrDecl(node, decl); - } - } - - 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[]) { - const attrParam = this.findAttrParamForArg(node); - const attrAppliedOn = node.$container.$container; - - 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 - // - // E.g.: - // - // model A { - // myId @id String - // } - // - // model B { - // id @id String - // a A @relation(fields: [id], references: [myId]) - // } - // - // In model B, the attribute argument "myId" is resolved to the field "myId" in model A - - 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); - if (isArrayExpr(node.value)) { - node.value.items.forEach((item) => { - if (isReferenceExpr(item)) { - const resolved = this.resolveFromScopeProviders(item, 'target', document, [scopeProvider]); - if (resolved) { - this.resolveToDeclaredType(item, (resolved as DataModelField).type); - } else { - // mark unresolvable - this.unresolvableRefExpr(item); - } - } - }); - if (node.value.items[0]?.$resolvedType?.decl) { - 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]); - if (resolved) { - this.resolveToDeclaredType(node.value, (resolved as DataModelField).type); - } else { - // mark unresolvable - this.unresolvableRefExpr(node.value); - } - } - } - } else { - this.resolve(node.value, document, extraScopes); - } - node.$resolvedType = node.value.$resolvedType; - } - - private unresolvableRefExpr(item: ReferenceExpr) { - const ref = item.target as DefaultReference; - ref._ref = this.createLinkingError({ - reference: ref, - container: item, - property: 'target', - }); - } - - private findAttrParamForArg(arg: AttributeArg): AttributeParam | undefined { - const attr = arg.$container.decl.ref; - if (!attr) { - return undefined; - } - if (arg.name) { - return attr.params?.find((p) => p.name === arg.name); - } else { - const index = arg.$container.args.findIndex((a) => a === arg); - return attr.params[index]; - } - } - - private resolveDataModel(node: DataModel, document: LangiumDocument, extraScopes: ScopeProvider[]) { - return this.resolveDefault(node, document, extraScopes); - } - - private resolveDataModelField( - node: DataModelField, - document: LangiumDocument, - 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 - // used as resolution target. The correct behavior is to resolve to the enum that's used - // as the declaration type of the field: - // - // enum FirstEnum { - // E1 - // E2 - // } - - // enum SecondEnum { - // E1 - // E3 - // E4 - // } - - // model M { - // id Int @id - // first SecondEnum @default(E1) <- should resolve to SecondEnum - // second FirstEnum @default(E1) <- should resolve to FirstEnum - // } - // - - // make sure type is resolved first - this.resolve(node.type, document, extraScopes); - - let scopes = extraScopes; - - // 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); - scopes = [enumScope, ...scopes]; - } - - this.resolveDefault(node, document, scopes); - } - - private resolveDefault(node: AstNode, document: LangiumDocument, extraScopes: ScopeProvider[]) { - for (const [property, value] of Object.entries(node)) { - if (!property.startsWith('$')) { - if (isReference(value)) { - this.linkReference(node, property, document, extraScopes); - } - } - } - for (const child of streamContents(node)) { - this.resolve(child, document, extraScopes); - } - } - - //#endregion - - //#region Utils - - private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataModelFieldType | TypeDefFieldType) { - let nullable = false; - if (isDataModelFieldType(type) || isTypeDefField(type)) { - nullable = type.optional; - - // referencing a field of 'Unsupported' type - if (type.unsupported) { - node.$resolvedType = { decl: 'Unsupported', array: type.array, nullable }; - return; - } - } - - if (type.type) { - const mappedType = mapBuiltinTypeToExpressionType(type.type); - node.$resolvedType = { decl: mappedType, array: type.array, nullable: nullable }; - } else if (type.reference) { - node.$resolvedType = { - decl: type.reference.ref, - array: type.array, - nullable: nullable, - }; - } - } - - private resolveToBuiltinTypeOrDecl(node: AstNode, type: ResolvedShape, array = false, nullable = false) { - node.$resolvedType = { decl: type, array, nullable }; - } - - //#endregion -} diff --git a/packages/schema/src/language-server/zmodel-module.ts b/packages/schema/src/language-server/zmodel-module.ts deleted file mode 100644 index 701d31d87..000000000 --- a/packages/schema/src/language-server/zmodel-module.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { ZModelGeneratedModule, ZModelGeneratedSharedModule } from '@zenstackhq/language/module'; -import { - DefaultConfigurationProvider, - DefaultDocumentBuilder, - DefaultFuzzyMatcher, - DefaultIndexManager, - DefaultLangiumDocumentFactory, - DefaultLangiumDocuments, - DefaultLanguageServer, - DefaultNodeKindProvider, - DefaultServiceRegistry, - DefaultSharedModuleContext, - DefaultWorkspaceSymbolProvider, - LangiumDefaultSharedServices, - LangiumServices, - LangiumSharedServices, - Module, - MutexLock, - PartialLangiumServices, - createGrammarConfig as createDefaultGrammarConfig, - createDefaultModule, - inject, -} from 'langium'; -import { TextDocuments } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; -import { ZModelValidationRegistry, ZModelValidator } from './validator/zmodel-validator'; -import { ZModelCodeActionProvider } from './zmodel-code-action'; -import { ZModelCompletionProvider } from './zmodel-completion-provider'; -import { ZModelDefinitionProvider } from './zmodel-definition'; -import { ZModelDocumentationProvider } from './zmodel-documentation-provider'; -import { ZModelFormatter } from './zmodel-formatter'; -import { ZModelHighlightProvider } from './zmodel-highlight'; -import { ZModelHoverProvider } from './zmodel-hover'; -import { ZModelLinker } from './zmodel-linker'; -import { ZModelScopeComputation, ZModelScopeProvider } from './zmodel-scope'; -import { ZModelSemanticTokenProvider } from './zmodel-semantic'; -import { ZModelWorkspaceManager } from './zmodel-workspace-manager'; - -/** - * Declaration of custom services - add your own service classes here. - */ -export type ZModelAddedServices = { - validation: { - ZModelValidator: ZModelValidator; - }; -}; - -/** - * Union of Langium default services and your custom services - use this as constructor parameter - * of custom service classes. - */ -export type ZModelServices = LangiumServices & ZModelAddedServices; - -/** - * Dependency injection module that overrides Langium default services and contributes the - * 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 ZModelModule: Module = { - references: { - ScopeComputation: (services) => new ZModelScopeComputation(services), - Linker: (services) => new ZModelLinker(services), - ScopeProvider: (services) => new ZModelScopeProvider(services), - }, - validation: { - ValidationRegistry: (services) => new ZModelValidationRegistry(services), - ZModelValidator: (services) => new ZModelValidator(services), - }, - lsp: { - Formatter: (services) => new ZModelFormatter(services), - CodeActionProvider: (services) => new ZModelCodeActionProvider(services), - DefinitionProvider: (services) => new ZModelDefinitionProvider(services), - SemanticTokenProvider: (services) => new ZModelSemanticTokenProvider(services), - CompletionProvider: (services) => new ZModelCompletionProvider(services), - HoverProvider: (services) => new ZModelHoverProvider(services), - DocumentHighlightProvider: (services) => new ZModelHighlightProvider(services), - }, - parser: { - GrammarConfig: (services) => createGrammarConfig(services), - }, - documentation: { - DocumentationProvider: (services) => new ZModelDocumentationProvider(services), - }, -}; - -// this duplicates createDefaultSharedModule except that a custom WorkspaceManager is used -// TODO: avoid this duplication -export function createSharedModule( - context: DefaultSharedModuleContext -): Module { - return { - ServiceRegistry: () => new DefaultServiceRegistry(), - lsp: { - Connection: () => context.connection, - LanguageServer: (services) => new DefaultLanguageServer(services), - WorkspaceSymbolProvider: (services) => new DefaultWorkspaceSymbolProvider(services), - NodeKindProvider: () => new DefaultNodeKindProvider(), - FuzzyMatcher: () => new DefaultFuzzyMatcher(), - }, - workspace: { - LangiumDocuments: (services) => new DefaultLangiumDocuments(services), - LangiumDocumentFactory: (services) => new DefaultLangiumDocumentFactory(services), - DocumentBuilder: (services) => new DefaultDocumentBuilder(services), - TextDocuments: () => new TextDocuments(TextDocument), - IndexManager: (services) => new DefaultIndexManager(services), - WorkspaceManager: (services) => new ZModelWorkspaceManager(services), - FileSystemProvider: (services) => context.fileSystemProvider(services), - MutexLock: () => new MutexLock(), - ConfigurationProvider: (services) => new DefaultConfigurationProvider(services), - }, - }; -} - -/** - * Create the full set of services required by Langium. - * - * First inject the shared services by merging two modules: - * - Langium default shared services - * - Services generated by langium-cli - * - * Then inject the language-specific services by merging three modules: - * - Langium default language-specific services - * - Services generated by langium-cli - * - Services specified in this file - * - * @param context Optional module context with the LSP connection - * @returns An object wrapping the shared services and the language-specific services - */ -export function createZModelServices(context: DefaultSharedModuleContext): { - shared: LangiumSharedServices; - ZModel: ZModelServices; -} { - const shared = inject(createSharedModule(context), ZModelGeneratedSharedModule); - - const ZModel = inject(createDefaultModule({ shared }), ZModelGeneratedModule, ZModelModule); - shared.ServiceRegistry.register(ZModel); - return { shared, ZModel }; -} - -function createGrammarConfig(services: LangiumServices) { - const config = createDefaultGrammarConfig(services); - config.nameRegexp = /^[@\w\p{L}]$/u; - return config; -} diff --git a/packages/schema/src/language-server/zmodel-scope.ts b/packages/schema/src/language-server/zmodel-scope.ts deleted file mode 100644 index 11cbb4909..000000000 --- a/packages/schema/src/language-server/zmodel-scope.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { - BinaryExpr, - MemberAccessExpr, - isDataModel, - isDataModelField, - isEnumField, - isInvocationExpr, - isMemberAccessExpr, - isModel, - isReferenceExpr, - isThisExpr, - isTypeDef, - isTypeDefField, -} from '@zenstackhq/language/ast'; -import { getAuthDecl, getModelFieldsWithBases, getRecursiveBases, isAuthInvocation } from '@zenstackhq/sdk'; -import { - AstNode, - AstNodeDescription, - DefaultScopeComputation, - DefaultScopeProvider, - EMPTY_SCOPE, - LangiumDocument, - LangiumServices, - PrecomputedScopes, - ReferenceInfo, - Scope, - StreamScope, - equalURI, - getContainerOfType, - interruptAndCheck, - stream, - streamAllContents, -} from 'langium'; -import { match } from 'ts-pattern'; -import { CancellationToken } from 'vscode-jsonrpc'; -import { - getAllLoadedAndReachableDataModelsAndTypeDefs, - isCollectionPredicate, - isFutureInvocation, - resolveImportUri, -} from '../utils/ast-utils'; -import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; -import { isAuthOrAuthMemberAccess } from './validator/utils'; - -/** - * Custom Langium ScopeComputation implementation which adds enum fields into global scope - */ -export class ZModelScopeComputation extends DefaultScopeComputation { - constructor(private readonly services: LangiumServices) { - super(services); - } - - async computeExports( - document: LangiumDocument, - cancelToken?: 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 streamAllContents(document.parseResult.value)) { - if (cancelToken) { - await interruptAndCheck(cancelToken); - } - if (isEnumField(node)) { - const desc = this.services.workspace.AstNodeDescriptionProvider.createDescription( - node, - node.name, - document - ); - result.push(desc); - } - } - - return result; - } - - override processNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes) { - super.processNode(node, document, scopes); - - if (isDataModel(node) && !node.$baseMerged) { - // add base fields to the scope recursively - const bases = getRecursiveBases(node); - for (const base of bases) { - for (const field of base.fields) { - scopes.add(node, this.descriptions.createDescription(field, this.nameProvider.getName(field))); - } - } - } - } -} - -export class ZModelScopeProvider extends DefaultScopeProvider { - constructor(private readonly services: LangiumServices) { - super(services); - } - - protected override getGlobalScope(referenceType: string, context: ReferenceInfo): Scope { - const model = getContainerOfType(context.container, isModel); - if (!model) { - return EMPTY_SCOPE; - } - - const importedUris = stream(model.imports).map(resolveImportUri).nonNullable(); - const importedElements = this.indexManager.allElements(referenceType).filter( - (des) => - // allow current document - equalURI(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) => equalURI(des.documentUri, importedUri)) - ); - return new StreamScope(importedElements); - } - - override getScope(context: ReferenceInfo): Scope { - if (isMemberAccessExpr(context.container) && context.container.operand && context.property === 'member') { - return this.getMemberAccessScope(context); - } - - 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); - if (containerCollectionPredicate) { - return this.getCollectionPredicateScope(context, containerCollectionPredicate); - } - } - - return super.getScope(context); - } - - private getMemberAccessScope(context: ReferenceInfo) { - const referenceType = this.reflection.getReferenceType(context); - const globalScope = this.getGlobalScope(referenceType, context); - const node = context.container as MemberAccessExpr; - - // typedef's fields are only added to the scope if the access starts with `auth().` - // or the member access resides inside a typedef - const allowTypeDefScope = isAuthOrAuthMemberAccess(node.operand) || !!getContainerOfType(node, isTypeDef); - - return match(node.operand) - .when(isReferenceExpr, (operand) => { - // 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 EMPTY_SCOPE; - }) - .when(isMemberAccessExpr, (operand) => { - // 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); - } - if (isTypeDefField(ref) && !ref.type.array) { - return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); - } - return EMPTY_SCOPE; - }) - .when(isThisExpr, () => { - // operand is `this`, resolve to the containing model - return this.createScopeForContainingModel(node, globalScope); - }) - .when(isInvocationExpr, (operand) => { - // deal with member access from `auth()` and `future() - if (isAuthInvocation(operand)) { - // resolve to `User` or `@@auth` decl - return this.createScopeForAuth(node, globalScope); - } - if (isFutureInvocation(operand)) { - // resolve `future()` to the containing model - return this.createScopeForContainingModel(node, globalScope); - } - return EMPTY_SCOPE; - }) - .otherwise(() => EMPTY_SCOPE); - } - - private getCollectionPredicateScope(context: ReferenceInfo, collectionPredicate: BinaryExpr) { - const referenceType = this.reflection.getReferenceType(context); - const globalScope = this.getGlobalScope(referenceType, context); - const collection = collectionPredicate.left; - - // typedef's fields are only added to the scope if the access starts with `auth().` - const allowTypeDefScope = isAuthOrAuthMemberAccess(collection); - - return match(collection) - .when(isReferenceExpr, (expr) => { - // 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 EMPTY_SCOPE; - }) - .when(isMemberAccessExpr, (expr) => { - // 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 EMPTY_SCOPE; - }) - .when(isAuthInvocation, (expr) => { - return this.createScopeForAuth(expr, globalScope); - }) - .otherwise(() => EMPTY_SCOPE); - } - - private createScopeForContainingModel(node: AstNode, globalScope: Scope) { - const model = getContainerOfType(node, isDataModel); - if (model) { - return this.createScopeForContainer(model, globalScope); - } else { - return EMPTY_SCOPE; - } - } - - private createScopeForContainer(node: AstNode | undefined, globalScope: Scope, includeTypeDefScope = false) { - if (isDataModel(node)) { - return this.createScopeForNodes(getModelFieldsWithBases(node), globalScope); - } else if (includeTypeDefScope && isTypeDef(node)) { - return this.createScopeForNodes(node.fields, globalScope); - } else { - return EMPTY_SCOPE; - } - } - - private createScopeForAuth(node: AstNode, globalScope: Scope) { - // get all data models and type defs from loaded and reachable documents - const decls = getAllLoadedAndReachableDataModelsAndTypeDefs( - this.services.shared.workspace.LangiumDocuments, - getContainerOfType(node, isDataModel) - ); - - const authDecl = getAuthDecl(decls); - if (authDecl) { - return this.createScopeForContainer(authDecl, globalScope, true); - } else { - return EMPTY_SCOPE; - } - } -} - -function getCollectionPredicateContext(node: AstNode) { - let curr: AstNode | undefined = node; - while (curr) { - if (curr.$container && isCollectionPredicate(curr.$container) && curr.$containerProperty === 'right') { - return curr.$container; - } - curr = curr.$container; - } - return undefined; -} diff --git a/packages/schema/src/language-server/zmodel-semantic.ts b/packages/schema/src/language-server/zmodel-semantic.ts deleted file mode 100644 index 2e24cdb7c..000000000 --- a/packages/schema/src/language-server/zmodel-semantic.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - isAttribute, - isAttributeArg, - isConfigField, - isDataModel, - isDataModelAttribute, - isDataModelField, - isDataModelFieldAttribute, - isDataModelFieldType, - isDataSource, - isEnum, - isEnumField, - isFunctionDecl, - isGeneratorDecl, - isInternalAttribute, - isInvocationExpr, - isMemberAccessExpr, - isPlugin, - isPluginField, - isReferenceExpr, - isTypeDef, - isTypeDefField, -} from '@zenstackhq/language/ast'; -import { AbstractSemanticTokenProvider, AstNode, SemanticTokenAcceptor } from 'langium'; -import { SemanticTokenTypes } from 'vscode-languageserver'; - -export class ZModelSemanticTokenProvider extends AbstractSemanticTokenProvider { - protected highlightElement(node: AstNode, acceptor: SemanticTokenAcceptor): void { - if (isDataModel(node)) { - acceptor({ - node, - property: 'name', - type: SemanticTokenTypes.type, - }); - - acceptor({ - node, - property: 'superTypes', - type: SemanticTokenTypes.type, - }); - } else if (isDataSource(node) || isGeneratorDecl(node) || isPlugin(node) || isEnum(node) || isTypeDef(node)) { - acceptor({ - node, - property: 'name', - type: SemanticTokenTypes.type, - }); - } else if ( - isDataModelField(node) || - isTypeDefField(node) || - isConfigField(node) || - isAttributeArg(node) || - isPluginField(node) || - isEnumField(node) - ) { - acceptor({ - node, - property: 'name', - type: SemanticTokenTypes.variable, - }); - } else if (isDataModelFieldType(node)) { - if (node.type) { - acceptor({ - node, - property: 'type', - type: SemanticTokenTypes.type, - }); - } else { - acceptor({ - node, - property: 'reference', - type: SemanticTokenTypes.macro, - }); - } - } else if (isDataModelAttribute(node) || isDataModelFieldAttribute(node) || isInternalAttribute(node)) { - acceptor({ - node, - property: 'decl', - type: SemanticTokenTypes.function, - }); - } else if (isInvocationExpr(node)) { - acceptor({ - node, - property: 'function', - type: SemanticTokenTypes.function, - }); - } else if (isFunctionDecl(node) || isAttribute(node)) { - acceptor({ - node, - property: 'name', - type: SemanticTokenTypes.function, - }); - } else if (isReferenceExpr(node)) { - acceptor({ - node, - property: 'target', - type: SemanticTokenTypes.variable, - }); - } else if (isMemberAccessExpr(node)) { - acceptor({ - node, - property: 'member', - type: SemanticTokenTypes.property, - }); - } - } -} diff --git a/packages/schema/src/language-server/zmodel-workspace-manager.ts b/packages/schema/src/language-server/zmodel-workspace-manager.ts deleted file mode 100644 index d8ba7bd8a..000000000 --- a/packages/schema/src/language-server/zmodel-workspace-manager.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { isPlugin, Model } from '@zenstackhq/language/ast'; -import { getLiteral } from '@zenstackhq/sdk'; -import { DefaultWorkspaceManager, interruptAndCheck, LangiumDocument } from 'langium'; -import fs from 'fs'; -import path from 'path'; -import { CancellationToken, WorkspaceFolder } from 'vscode-languageserver'; -import { URI, Utils } from 'vscode-uri'; -import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; - -/** - * Custom Langium WorkspaceManager implementation which automatically loads stdlib.zmodel - */ -export class ZModelWorkspaceManager extends DefaultWorkspaceManager { - public pluginModels = new Set(); - - protected async loadAdditionalDocuments( - _folders: WorkspaceFolder[], - _collector: (document: LangiumDocument) => void - ): Promise { - await super.loadAdditionalDocuments(_folders, _collector); - - let stdLibPath: string; - // First, try to find the stdlib from an installed zenstack package - // in the project's node_modules - let installedStdlibPath: string | undefined; - for (const folder of _folders) { - const folderPath = this.getRootFolder(folder).fsPath; - try { - // Try to resolve zenstack from the workspace folder - const languagePackagePath = require.resolve('zenstack/package.json', { - paths: [folderPath] - }); - const languagePackageDir = path.dirname(languagePackagePath); - 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}`); - break; - } - } catch (error) { - // Package not found or other error, continue to next folder - continue; - } - } - - if (installedStdlibPath) { - stdLibPath = installedStdlibPath; - } else { - // Fallback to bundled stdlib - stdLibPath = path.join(__dirname, '../res', STD_LIB_MODULE_NAME); - console.log(`Using bundled stdlib in extension`); - } - - const stdLibUri = URI.file(stdLibPath); - console.log(`Adding stdlib document from ${stdLibUri}`); - const stdlib = this.langiumDocuments.getOrCreateDocument(stdLibUri); - _collector(stdlib); - } - - override async initializeWorkspace( - folders: WorkspaceFolder[], - cancelToken = CancellationToken.None - ): Promise { - const fileExtensions = this.serviceRegistry.all.flatMap((e) => e.LanguageMetaData.fileExtensions); - const documents: LangiumDocument[] = []; - const collector = (document: LangiumDocument) => { - documents.push(document); - if (!this.langiumDocuments.hasDocument(document.uri)) { - this.langiumDocuments.addDocument(document); - } - }; - - await this.loadAdditionalDocuments(folders, collector); - await Promise.all( - folders - .map((wf) => [wf, this.getRootFolder(wf)] as [WorkspaceFolder, URI]) - .map(async (entry) => this.traverseFolder(...entry, fileExtensions, collector)) - ); - - // find plugin models - documents.forEach((doc) => { - const parsed = doc.parseResult.value as Model; - parsed.declarations.forEach((decl) => { - if (isPlugin(decl)) { - const providerField = decl.fields.find((f) => f.name === 'provider'); - if (providerField) { - const provider = getLiteral(providerField.value); - if (provider) { - this.pluginModels.add(provider); - } - } - } - }); - }); - - if (this.pluginModels.size > 0) { - console.log(`Used plugin documents: ${Array.from(this.pluginModels)}`); - - // the loaded plugin models would be removed from the set - const unLoadedPluginModels = new Set(this.pluginModels); - - await Promise.all( - folders - .map((wf) => [wf, this.getRootFolder(wf)] as [WorkspaceFolder, URI]) - .map(async (entry) => this.loadPluginModels(...entry, unLoadedPluginModels, collector)) - ); - - if (unLoadedPluginModels.size > 0) { - console.warn(`The following plugin documents could not be loaded: ${Array.from(unLoadedPluginModels)}`); - } - } - - // Only after creating all documents do we check whether we need to cancel the initialization - // The document builder will later pick up on all unprocessed documents - await interruptAndCheck(cancelToken); - await this.documentBuilder.build(documents, undefined, cancelToken); - } - - protected async loadPluginModels( - workspaceFolder: WorkspaceFolder, - folderPath: URI, - pluginModels: Set, - collector: (document: LangiumDocument) => void - ): Promise { - const content = await ( - await this.fileSystemProvider.readDirectory(folderPath) - ).sort((a, b) => { - // make sure the node_moudules folder is always the first one to be checked - // so it could be early exited if the plugin is found - if (a.isDirectory && b.isDirectory) { - const aName = Utils.basename(a.uri); - if (aName === 'node_modules') { - return -1; - } else { - return 1; - } - } else { - return 0; - } - }); - - for (const entry of content) { - if (entry.isDirectory) { - const name = Utils.basename(entry.uri); - if (name === 'node_modules') { - for (const plugin of Array.from(pluginModels)) { - const path = Utils.joinPath(entry.uri, plugin, PLUGIN_MODULE_NAME); - try { - this.fileSystemProvider.readFileSync(path); - const document = this.langiumDocuments.getOrCreateDocument(path); - collector(document); - console.log(`Adding plugin document from ${path.path}`); - - pluginModels.delete(plugin); - // early exit if all plugins are loaded - if (pluginModels.size === 0) { - return; - } - } catch { - // no-op. The module might be found in another node_modules folder - // will show the warning message eventually if not found - } - } - } else { - await this.loadPluginModels(workspaceFolder, entry.uri, pluginModels, collector); - } - } - } - } -} diff --git a/packages/schema/src/package.json b/packages/schema/src/package.json deleted file mode 120000 index 4e26811d4..000000000 --- a/packages/schema/src/package.json +++ /dev/null @@ -1 +0,0 @@ -../package.json \ No newline at end of file diff --git a/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts b/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts deleted file mode 100644 index 94e73c1ba..000000000 --- a/packages/schema/src/plugins/enhancer/enhance/auth-type-generator.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { getIdFields, getPrismaClientGenerator, isAuthInvocation, isDataModelFieldReference } from '@zenstackhq/sdk'; -import { - DataModel, - DataModelField, - Expression, - isDataModel, - isMemberAccessExpr, - isTypeDef, - TypeDef, - type Model, -} from '@zenstackhq/sdk/ast'; -import { streamAst, type AstNode } from 'langium'; -import { isCollectionPredicate } from '../../../utils/ast-utils'; - -/** - * Generate types for typing the `user` context object passed to the `enhance` call, based - * on the fields (potentially deeply) access through `auth()`. - */ -export function generateAuthType(model: Model, authDecl: DataModel | TypeDef) { - const types = new Map< - string, - { - isTypeDef: boolean; - // relation fields to require - requiredRelations: { name: string; type: string }[]; - } - >(); - - types.set(authDecl.name, { isTypeDef: isTypeDef(authDecl), requiredRelations: [] }); - - const findType = (name: string) => - model.declarations.find((d) => (isDataModel(d) || isTypeDef(d)) && d.name === name); - - const ensureType = (name: string) => { - if (!types.has(name)) { - const decl = findType(name); - if (!decl) { - return; - } - types.set(name, { isTypeDef: isTypeDef(decl), requiredRelations: [] }); - } - }; - - const addTypeField = (typeName: string, fieldName: string, fieldType: string, array: boolean) => { - let typeInfo = types.get(typeName); - if (!typeInfo) { - const decl = findType(typeName); - if (!decl) { - return; - } - typeInfo = { isTypeDef: isTypeDef(decl), requiredRelations: [] }; - types.set(typeName, typeInfo); - } - if (!typeInfo.requiredRelations.find((f) => f.name === fieldName)) { - typeInfo.requiredRelations.push({ name: fieldName, type: array ? `${fieldType}[]` : fieldType }); - } - }; - - // get all policy expressions involving `auth()` - const authInvolvedExprs = streamAst(model).filter(isAuthAccess); - - // traverse the expressions and collect types and fields involved - authInvolvedExprs.forEach((expr) => { - streamAst(expr).forEach((node) => { - if (isMemberAccessExpr(node)) { - const exprType = node.operand.$resolvedType?.decl; - if (isDataModel(exprType)) { - const memberDecl = node.member.ref; - if (isDataModel(memberDecl?.type.reference?.ref)) { - // member is a relation - const fieldType = memberDecl.type.reference.ref.name; - ensureType(fieldType); - addTypeField(exprType.name, memberDecl.name, fieldType, memberDecl.type.array); - } - } - } - - if (isDataModelFieldReference(node)) { - // this can happen inside collection predicates - const fieldDecl = node.target.ref as DataModelField; - const fieldType = fieldDecl.type.reference?.ref; - if (isDataModel(fieldType)) { - // field is a relation - ensureType(fieldType.name); - addTypeField(fieldDecl.$container.name, node.target.$refText, fieldType.name, fieldDecl.type.array); - } - } - }); - }); - - const prismaGenerator = getPrismaClientGenerator(model); - const isNewGenerator = !!prismaGenerator?.isNewGenerator; - - // generate: - // ` - // namespace auth { - // export type User = WithRequired, 'id'> & { profile: Profile; } & Record; - // export type Profile = WithRequired, 'age'> & Record; - // } - // ` - - return `export namespace auth { - type WithRequired = T & { [P in K]-?: T[P] }; -${Array.from(types.entries()) - .map(([type, typeInfo]) => { - // TypeDef types are generated in "json-types.ts" for the new "prisma-client" generator - const typeRef = isNewGenerator && typeInfo.isTypeDef ? `$TypeDefs.${type}` : `_P.${type}`; - let result = `Partial<${typeRef}>`; - - if (type === authDecl.name) { - // auth model's id fields are always required - const idFields = getIdFields(authDecl).map((f) => f.name); - if (idFields.length > 0) { - result = `WithRequired<${result}, ${idFields.map((f) => `'${f}'`).join('|')}>`; - } - } - - if (typeInfo.requiredRelations.length > 0) { - // merge required relation fields - result = `${result} & { ${typeInfo.requiredRelations.map((f) => `${f.name}: ${f.type}`).join('; ')} }`; - } - - result = `${result} & Record`; - - return ` export type ${type} = ${result};`; - }) - .join('\n')} -}`; -} - -function isAuthAccess(node: AstNode): node is Expression { - if (isAuthInvocation(node)) { - return true; - } - - if (isMemberAccessExpr(node) && isAuthAccess(node.operand)) { - return true; - } - - if (isCollectionPredicate(node)) { - if (isAuthAccess(node.left)) { - return true; - } - } - - return false; -} diff --git a/packages/schema/src/plugins/enhancer/enhance/checker-type-generator.ts b/packages/schema/src/plugins/enhancer/enhance/checker-type-generator.ts deleted file mode 100644 index d65b84cf4..000000000 --- a/packages/schema/src/plugins/enhancer/enhance/checker-type-generator.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getDataModels } from '@zenstackhq/sdk'; -import type { DataModel, DataModelField, Model } from '@zenstackhq/sdk/ast'; -import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { P, match } from 'ts-pattern'; - -/** - * Generates a `ModelCheckers` interface that contains a `check` method for each model in the schema. - * - * E.g.: - * - * ```ts - * type CheckerOperation = 'create' | 'read' | 'update' | 'delete'; - * - * export interface ModelCheckers { - * user: { check(op: CheckerOperation, args?: { email?: string; age?: number; }): Promise }, - * ... - * } - * ``` - */ -export function generateCheckerType(model: Model) { - return ` -import type { PolicyCrudKind } from '@zenstackhq/runtime'; - -export interface ModelCheckers { - ${getDataModels(model) - .map((dataModel) => `\t${lowerCaseFirst(dataModel.name)}: ${generateDataModelChecker(dataModel)}`) - .join(',\n')} -} -`; -} - -function generateDataModelChecker(dataModel: DataModel) { - return `{ - check(args: { operation: PolicyCrudKind, where?: ${generateDataModelArgs(dataModel)} }): Promise - }`; -} - -function generateDataModelArgs(dataModel: DataModel) { - return `{ ${dataModel.fields - .filter((field) => isFieldFilterable(field)) - .map((field) => `${field.name}?: ${mapFieldType(field)}`) - .join('; ')} }`; -} - -function isFieldFilterable(field: DataModelField) { - return !!mapFieldType(field); -} - -function mapFieldType(field: DataModelField) { - return match(field.type.type) - .with('Boolean', () => 'boolean') - .with(P.union('BigInt', 'Int', 'Float', 'Decimal'), () => 'number') - .with('String', () => 'string') - .otherwise(() => undefined); -} diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts deleted file mode 100644 index 0f556efbf..000000000 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ /dev/null @@ -1,1135 +0,0 @@ -import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; -import { invariant, upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { - PluginError, - getAttribute, - getAttributeArg, - getAuthDecl, - getDataModelAndTypeDefs, - getDataModels, - getForeignKeyFields, - getPrismaClientGenerator, - getRelationField, - hasAttribute, - isDelegateModel, - isDiscriminatorField, - normalizedRelative, - saveSourceFile, - type PluginOptions, -} from '@zenstackhq/sdk'; -import { - DataModel, - DataModelField, - ReferenceExpr, - isArrayExpr, - isDataModel, - isTypeDef, - type Model, -} from '@zenstackhq/sdk/ast'; -import { getDMMF, getPrismaClientImportSpec, getPrismaVersion, type DMMF } from '@zenstackhq/sdk/prisma'; -import fs from 'fs'; -import path from 'path'; -import semver from 'semver'; -import { - FunctionDeclarationStructure, - InterfaceDeclaration, - ModuleDeclaration, - Node, - Project, - SourceFile, - SyntaxKind, - TypeAliasDeclaration, - VariableStatement, - type StatementStructures, -} from 'ts-morph'; -import { name } from '..'; -import { getConcreteModels, getDiscriminatorField } from '../../../utils/ast-utils'; -import { execPackage } from '../../../utils/exec-utils'; -import { CorePlugins, getPluginCustomOutputFolder } from '../../plugin-utils'; -import { trackPrismaSchemaError } from '../../prisma'; -import { PrismaSchemaGenerator } from '../../prisma/schema-generator'; -import { isDefaultWithAuth } from '../enhancer-utils'; -import { generateAuthType } from './auth-type-generator'; -import { generateCheckerType } from './checker-type-generator'; -import { generateTypeDefType } from './model-typedef-generator'; - -// information of delegate models and their sub models -type DelegateInfo = [DataModel, DataModel[]][]; - -const LOGICAL_CLIENT_GENERATION_PATH = './logical-prisma-client'; - -export class EnhancerGenerator { - // regex for matching "ModelCreateXXXInput" and "ModelUncheckedCreateXXXInput" type - // names for models that use `auth()` in `@default` attribute - private readonly modelsWithAuthInDefaultCreateInputPattern: RegExp; - - // models with JSON type fields - private readonly modelsWithJsonTypeFields: DataModel[]; - - // Regex patterns for matching input/output types for models with JSON type fields - private readonly modelsWithJsonTypeFieldsInputOutputPattern: RegExp[]; - - // a mapping from shortened names to full names - private reversedShortNameMap = new Map(); - - constructor( - private readonly model: Model, - private readonly options: PluginOptions, - private readonly project: Project, - private readonly outDir: string - ) { - const modelsWithAuthInDefault = this.model.declarations.filter( - (d): d is DataModel => isDataModel(d) && d.fields.some((f) => f.attributes.some(isDefaultWithAuth)) - ); - - this.modelsWithAuthInDefaultCreateInputPattern = new RegExp( - `^(${modelsWithAuthInDefault.map((m) => m.name).join('|')})(Unchecked)?Create.*?Input$` - ); - - this.modelsWithJsonTypeFields = this.model.declarations.filter( - (d): d is DataModel => isDataModel(d) && d.fields.some((f) => isTypeDef(f.type.reference?.ref)) - ); - - // input/output patterns for models with json type fields - const relevantTypePatterns = [ - 'GroupByOutputType', - '(Unchecked)?Create(\\S+?)?Input', - '(Unchecked)?Update(\\S+?)?Input', - 'CreateManyInput', - '(Unchecked)?UpdateMany(Mutation)?Input', - ]; - // build combination regex with all models with JSON types and the above suffixes - this.modelsWithJsonTypeFieldsInputOutputPattern = this.modelsWithJsonTypeFields.map( - (m) => new RegExp(`^(${m.name})(${relevantTypePatterns.join('|')})$`) - ); - } - - async generate(): Promise<{ dmmf: DMMF.Document | undefined; newPrismaClientDtsPath: string | undefined }> { - if (this.isNewPrismaClientGenerator) { - // "prisma-client" generator - return this.generateForNewClientGenerator(); - } else { - // "prisma-client-js" generator - return this.generateForOldClientGenerator(); - } - } - - // logic for "prisma-client" generator - private async generateForNewClientGenerator() { - const needsLogicalClient = this.needsLogicalClient; - const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); - let resultPrismaBaseImport = path.dirname(prismaImport); // get to the parent folder of "client" - let dmmf: DMMF.Document | undefined; - - if (needsLogicalClient) { - // use logical client, note we use the parent of "client" folder here too - resultPrismaBaseImport = LOGICAL_CLIENT_GENERATION_PATH; - const result = await this.generateLogicalPrisma(); - dmmf = result.dmmf; - } - - // `models.ts` for exporting model types - - const modelsTsContent = [`export * from '${resultPrismaBaseImport}/models';`]; - if (this.model.declarations.some((d) => isTypeDef(d))) { - modelsTsContent.push(`export * from './json-types';`); - } - - const modelsTs = this.project.createSourceFile( - path.join(this.outDir, 'models.ts'), - modelsTsContent.join('\n'), - { - overwrite: true, - } - ); - this.saveSourceFile(modelsTs); - - // `enums.ts` for exporting enums - const enumsTs = this.project.createSourceFile( - path.join(this.outDir, 'enums.ts'), - `export * from '${resultPrismaBaseImport}/enums';`, - { - overwrite: true, - } - ); - this.saveSourceFile(enumsTs); - - // `client.ts` for exporting `PrismaClient` and `Prisma` namespace - const clientTs = this.project.createSourceFile( - path.join(this.outDir, 'client.ts'), - `export * from '${resultPrismaBaseImport}/client';`, - { - overwrite: true, - } - ); - this.saveSourceFile(clientTs); - - this.generateEnhance(prismaImport, `${resultPrismaBaseImport}/client`, needsLogicalClient, 'node'); - - return { - // logical dmmf if there is one - dmmf, - // new client generator doesn't have a barrel .d.ts file - newPrismaClientDtsPath: undefined, - }; - } - - // logic for "prisma-client-js" generator - private async generateForOldClientGenerator() { - const needsLogicalClient = this.needsLogicalClient; - const prismaImport = getPrismaClientImportSpec(this.outDir, this.options); - let resultPrismaClientImport = prismaImport; - let dmmf: DMMF.Document | undefined; - - if (needsLogicalClient) { - // redirect `PrismaClient` import to the logical client - resultPrismaClientImport = LOGICAL_CLIENT_GENERATION_PATH; - const result = await this.generateLogicalPrisma(); - dmmf = result.dmmf; - } - - // `models.ts` for exporting model types - const modelsTsContent = `export * from '${resultPrismaClientImport}';`; - const modelsTs = this.project.createSourceFile(path.join(this.outDir, 'models.ts'), modelsTsContent, { - overwrite: true, - }); - this.saveSourceFile(modelsTs); - - // `enhance.ts` and `enhance-edge.ts` - for (const target of ['node', 'edge'] as const) { - this.generateEnhance(prismaImport, resultPrismaClientImport, needsLogicalClient, target); - } - - return { - // logical dmmf if there is one - dmmf, - newPrismaClientDtsPath: needsLogicalClient - ? path.resolve(this.outDir, LOGICAL_CLIENT_GENERATION_PATH, 'index.d.ts') - : undefined, - }; - } - - private generateEnhance( - prismaImport: string, - prismaClientImport: string, - needsLogicalClient: boolean, - target: 'node' | 'edge' - ) { - const authDecl = getAuthDecl(getDataModelAndTypeDefs(this.model)); - const authTypes = authDecl ? generateAuthType(this.model, authDecl) : ''; - const authTypeParam = authDecl ? `auth.${authDecl.name}` : 'AuthUser'; - const checkerTypes = this.generatePermissionChecker ? generateCheckerType(this.model) : ''; - - const outFile = target === 'node' ? 'enhance.ts' : 'enhance-edge.ts'; - const enhanceTs = this.project.createSourceFile( - path.join(this.outDir, outFile), - `/* eslint-disable */ -import { type EnhancementContext, type EnhancementOptions, type ZodSchemas, type AuthUser } from '@zenstackhq/runtime'; -import { createEnhancement } from '@zenstackhq/runtime/enhancements/${target}'; -import modelMeta from './model-meta'; -import policy from './policy'; -${ - this.options.withZodSchemas - ? `import * as zodSchemas from '${this.getZodImport()}';` - : 'const zodSchemas = undefined;' -} - -${ - needsLogicalClient - ? this.createLogicalPrismaImports(prismaImport, prismaClientImport, target) - : this.createSimplePrismaImports(prismaImport, target) -} - -${authTypes} - -${checkerTypes} - -${ - needsLogicalClient - ? this.createLogicalPrismaEnhanceFunction(authTypeParam) - : this.createSimplePrismaEnhanceFunction(authTypeParam) -} - `, - { overwrite: true } - ); - - this.saveSourceFile(enhanceTs); - } - - private getZodImport() { - const zodCustomOutput = getPluginCustomOutputFolder(this.model, CorePlugins.Zod); - - if (!this.options.output && !zodCustomOutput) { - // neither zod or me (enhancer) have custom output, use the default - return './zod'; - } - - if (!zodCustomOutput) { - // I have a custom output, but zod doesn't, import from runtime - return '@zenstackhq/runtime/zod'; - } - - if (!this.options.output) { - // I don't have a custom output, but zod has, CLI will still generate - // a copy into the default output, so we can still import from there - return './zod'; - } - - // both zod and me have custom output, resolve to relative path and import - const schemaDir = path.dirname(this.options.schemaPath); - const zodAbsPath = path.isAbsolute(zodCustomOutput) - ? zodCustomOutput - : path.resolve(schemaDir, zodCustomOutput); - return normalizedRelative(this.outDir, zodAbsPath); - } - - private createSimplePrismaImports(prismaImport: string, target: string | undefined) { - const prismaTargetImport = target === 'edge' ? `${prismaImport}/edge` : prismaImport; - - return `import { Prisma, type PrismaClient } from '${prismaTargetImport}'; -import type * as _P from '${prismaImport}'; -export type { PrismaClient }; - -/** - * Infers the type of PrismaClient with ZenStack's enhancements. - * @example - * type EnhancedPrismaClient = Enhanced; - */ -export type Enhanced = Client; - `; - } - - private createSimplePrismaEnhanceFunction(authTypeParam: string) { - const returnType = `DbClient${this.generatePermissionChecker ? ' & ModelCheckers' : ''}`; - return ` -export function enhance(prisma: DbClient, context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions): ${returnType} { - return createEnhancement(prisma, { - modelMeta, - policy, - zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), - prismaModule: Prisma, - ...options - }, context) as ${returnType}; -} - `; - } - - private createLogicalPrismaImports(prismaImport: string, prismaClientImport: string, target: string | undefined) { - const prismaVersion = getPrismaVersion(); - const runtimeLibraryImportSubPath = prismaVersion && semver.gte(prismaVersion, '7.0.0') ? '/runtime/client' : '/runtime/library'; - - const prismaTargetImport = target === 'edge' ? `${prismaImport}/edge` : prismaImport; - const runtimeLibraryImport = this.isNewPrismaClientGenerator - ? // new generator has these types only in "@prisma/client" - `@prisma/client${runtimeLibraryImportSubPath}` - : // old generator has these types generated with the client - `${prismaImport}/runtime/library`; - - const hasTypeDef = this.model.declarations.some(isTypeDef); - - return `import { Prisma as _Prisma, PrismaClient as _PrismaClient } from '${prismaTargetImport}'; -import type { InternalArgs, DynamicClientExtensionThis } from '${runtimeLibraryImport}'; -import type * as _P from '${prismaClientImport}';${ - hasTypeDef && this.isNewPrismaClientGenerator ? `\nimport type * as $TypeDefs from './json-types';` : '' - } -import type { Prisma, PrismaClient } from '${prismaClientImport}'; -export type { PrismaClient }; -`; - } - - private createLogicalPrismaEnhanceFunction(authTypeParam: string) { - const prismaVersion = getPrismaVersion(); - - // Prisma 5.16.0...6.5.0 introduced a new generic parameter to `DynamicClientExtensionThis` - const hasClientOptions = - prismaVersion && semver.gte(prismaVersion, '5.16.0') && semver.lt(prismaVersion, '6.5.0'); - - return ` -// overload for plain PrismaClient -export function enhance & InternalArgs>( - prisma: _PrismaClient${this.isNewPrismaClientGenerator ? '' : ''}, - context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions): PrismaClient${ - this.generatePermissionChecker ? ' & ModelCheckers' : '' - }; - -// overload for extended PrismaClient -export function enhance & InternalArgs${hasClientOptions ? ', ClientOptions' : ''}>( - prisma: DynamicClientExtensionThis<_Prisma.TypeMap, _Prisma.TypeMapCb, ExtArgs${ - hasClientOptions ? ', ClientOptions' : '' - }>, - context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions): DynamicClientExtensionThis, Prisma.TypeMapCb, ExtArgs${ - hasClientOptions ? ', ClientOptions' : '' - }>${this.generatePermissionChecker ? ' & ModelCheckers' : ''}; - -export function enhance(prisma: any, context?: EnhancementContext<${authTypeParam}>, options?: EnhancementOptions): any { - return createEnhancement(prisma, { - modelMeta, - policy, - zodSchemas: zodSchemas as unknown as (ZodSchemas | undefined), - prismaModule: _Prisma, - ...options - }, context); -} - -/** - * Infers the type of PrismaClient with ZenStack's enhancements. - * @example - * type EnhancedPrismaClient = Enhanced; - */ -export type Enhanced = - Client extends _PrismaClient ? PrismaClient : - Client extends DynamicClientExtensionThis<_Prisma.TypeMap, infer _TypeMapCb, infer ExtArgs${ - hasClientOptions ? ', infer ClientOptions' : '' - }> ? DynamicClientExtensionThis, Prisma.TypeMapCb, ExtArgs${ - hasClientOptions ? ', ClientOptions' : '' - }> : Client; -`; - } - - private get needsLogicalClient() { - return this.hasDelegateModel(this.model) || this.hasAuthInDefault(this.model) || this.hasTypeDef(this.model); - } - - private hasDelegateModel(model: Model) { - const dataModels = getDataModels(model); - return dataModels.some( - (dm) => isDelegateModel(dm) && dataModels.some((sub) => sub.superTypes.some((base) => base.ref === dm)) - ); - } - - private hasAuthInDefault(model: Model) { - return getDataModels(model).some((dm) => - dm.fields.some((f) => f.attributes.some((attr) => isDefaultWithAuth(attr))) - ); - } - - private hasTypeDef(model: Model) { - return model.declarations.some(isTypeDef); - } - - private async generateLogicalPrisma() { - const prismaGenerator = new PrismaSchemaGenerator(this.model); - - // dir of the zmodel file - const zmodelDir = path.dirname(this.options.schemaPath); - - // generate a temp logical prisma schema in zmodel's dir - const logicalPrismaFile = path.join(zmodelDir, `logical-${Date.now()}.prisma`); - - // calculate a relative output path to output the logical prisma client into enhancer's output dir - const prismaClientOutDir = path.join(path.relative(zmodelDir, this.outDir), LOGICAL_CLIENT_GENERATION_PATH); - const generateResult = await prismaGenerator.generate({ - provider: '@internal', // doesn't matter - schemaPath: this.options.schemaPath, - output: logicalPrismaFile, - overrideClientGenerationPath: prismaClientOutDir, - mode: 'logical', - customAttributesAsComments: true, - }); - - // reverse direction of shortNameMap and store for future lookup - this.reversedShortNameMap = new Map( - Array.from(generateResult.shortNameMap.entries()).map(([key, value]) => [value, key]) - ); - - // generate the prisma client - - // only run prisma client generator for the logical schema - const prismaClientGeneratorName = this.getPrismaClientGeneratorName(this.model); - let generateCmd = `prisma generate --schema "${logicalPrismaFile}" --generator=${prismaClientGeneratorName}`; - - const prismaVersion = getPrismaVersion(); - if (!prismaVersion || (semver.gte(prismaVersion, '5.2.0') && semver.lt(prismaVersion, '7.0.0'))) { - // add --no-engine to reduce generation size if the prisma version supports - // v7 has removed this option completely, because it no longer generates an engine - generateCmd += ' --no-engine'; - } - - try { - // run 'prisma generate' - await execPackage(generateCmd, { stdio: 'ignore' }); - } catch { - await trackPrismaSchemaError(logicalPrismaFile); - try { - // run 'prisma generate' again with output to the console - await execPackage(generateCmd); - } catch { - // noop - } - throw new PluginError(name, `Failed to run "prisma generate" on logical schema: ${logicalPrismaFile}`); - } - - // make a bunch of typing fixes to the generated prisma client - await this.processClientTypes(path.join(this.outDir, LOGICAL_CLIENT_GENERATION_PATH)); - - // get the dmmf of the logical prisma schema - const dmmf = await this.getLogicalDMMF(logicalPrismaFile); - - try { - // clean up temp schema - if (fs.existsSync(logicalPrismaFile)) { - fs.rmSync(logicalPrismaFile); - } - } catch { - // ignore errors - } - - return { - prismaSchema: logicalPrismaFile, - // load the dmmf of the logical prisma schema - dmmf, - }; - } - - private async getLogicalDMMF(logicalPrismaFile: string) { - const dmmf = await getDMMF({ datamodel: fs.readFileSync(logicalPrismaFile, { encoding: 'utf-8' }) }); - - // make necessary fixes - - // fields that use `auth()` in `@default` are not handled by Prisma so in the DMMF - // they may be incorrectly represented as required, we need to fix that for input types - // also, if a FK field is of such case, its corresponding relation field should be optional - const createInputPattern = new RegExp(`^(.+?)(Unchecked)?Create.*Input$`); - for (const inputType of dmmf.schema.inputObjectTypes.prisma ?? []) { - const match = inputType.name.match(createInputPattern); - const modelName = this.resolveName(match?.[1]); - if (modelName) { - const dataModel = this.model.declarations.find( - (d): d is DataModel => isDataModel(d) && d.name === modelName - ); - if (dataModel) { - for (const field of inputType.fields) { - if (field.isRequired && this.shouldBeOptional(field, dataModel)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (field as any).isRequired = false; - } - } - } - } - } - return dmmf; - } - - private shouldBeOptional(field: DMMF.SchemaArg, dataModel: DataModel) { - const dmField = dataModel.fields.find((f) => f.name === field.name); - if (!dmField) { - return false; - } - - if (hasAttribute(dmField, '@default')) { - return true; - } - - if (isDataModel(dmField.type.reference?.ref)) { - // if FK field should be optional, the relation field should too - const fkFields = getForeignKeyFields(dmField); - if (fkFields.length > 0 && fkFields.every((f) => hasAttribute(f, '@default'))) { - return true; - } - } - - return false; - } - - private getPrismaClientGeneratorName(model: Model) { - const gen = getPrismaClientGenerator(model); - if (!gen) { - throw new PluginError(name, `Cannot find "prisma-client-js" or "prisma-client" generator in the schema`); - } - return gen.name; - } - - private async processClientTypes(prismaClientDir: string) { - // build a map of delegate models and their sub models - const delegateInfo: DelegateInfo = []; - this.model.declarations - .filter((d): d is DataModel => isDelegateModel(d)) - .forEach((dm) => { - const concreteModels = getConcreteModels(dm); - if (concreteModels.length > 0) { - delegateInfo.push([dm, concreteModels]); - } - }); - - if (this.isNewPrismaClientGenerator) { - await this.processClientTypesNewPrismaGenerator(prismaClientDir, delegateInfo); - } else { - await this.processClientTypesLegacyPrismaGenerator(prismaClientDir, delegateInfo); - } - } - private async processClientTypesLegacyPrismaGenerator(prismaClientDir: string, delegateInfo: DelegateInfo) { - const project = new Project(); - const sf = project.addSourceFileAtPath(path.join(prismaClientDir, 'index.d.ts')); - - // transform index.d.ts and write it into a new file (better perf than in-line editing) - const sfNew = project.createSourceFile(path.join(prismaClientDir, 'index-fixed.d.ts'), undefined, { - overwrite: true, - }); - - this.transformPrismaTypes(sf, sfNew, delegateInfo); - - this.generateExtraTypes(sfNew); - - sfNew.formatText(); - - // Save the transformed file over the original - await sfNew.move(sf.getFilePath(), { overwrite: true }); - await sfNew.save(); - } - - private async processClientTypesNewPrismaGenerator(prismaClientDir: string, delegateInfo: DelegateInfo) { - const project = new Project(); - - // remove delegate_aux_* fields from the prismaNamespace.ts - const internalFilename = `${prismaClientDir}/internal/prismaNamespace.ts`; - const internalFilenameFixed = `${prismaClientDir}/internal/prismaNamespace-fixed.ts`; - const internalSf = project.addSourceFileAtPath(internalFilename); - const syntaxList = internalSf.getChildren()[0]; - if (!Node.isSyntaxList(syntaxList)) { - throw new PluginError(name, `Unexpected syntax list structure in ${internalFilename}`); - } - const statements: (string | StatementStructures)[] = []; - - syntaxList.getChildren().forEach((node) => { - if (Node.isVariableStatement(node)) { - statements.push(this.transformVariableStatementProps(node)); - } else { - statements.push(node.getText()); - } - }); - const structure = internalSf.getStructure(); - structure.statements = statements; - - const internalSfNew = project.createSourceFile(internalFilenameFixed, structure, { - overwrite: true, - }); - await internalSfNew.save(); - fs.renameSync(internalFilenameFixed, internalFilename); - - // Create a shared file for all JSON fields type definitions - if (this.model.declarations.some(isTypeDef)) { - const jsonFieldsFile = project.createSourceFile(path.join(this.outDir, 'json-types.ts'), undefined, { - overwrite: true, - }); - this.generateExtraTypes(jsonFieldsFile); - await saveSourceFile(jsonFieldsFile); - } - - for (const d of this.model.declarations.filter(isDataModel)) { - const fileName = `${prismaClientDir}/models/${d.name}.ts`; - const sf = project.addSourceFileAtPath(fileName); - - const syntaxList = sf.getChildren()[0]; - if (!Node.isSyntaxList(syntaxList)) { - throw new PluginError(name, `Unexpected syntax list structure in ${fileName}`); - } - - const statements: (string | StatementStructures)[] = []; - - // Add import for json-types if this model has JSON type fields - const modelWithJsonFields = this.modelsWithJsonTypeFields.find((m) => m.name === d.name); - if (modelWithJsonFields) { - // Get the specific types that are used in this model - const getTypedJsonFields = (model: DataModel) => { - return model.fields.filter((f) => isTypeDef(f.type.reference?.ref)); - }; - const jsonFieldTypes = getTypedJsonFields(modelWithJsonFields); - const typeNames = [...new Set(jsonFieldTypes.map((field) => field.type.reference!.$refText))]; - - if (typeNames.length > 0) { - statements.push(`import type { ${typeNames.join(', ')} } from "../../json-types";`); - } - } - - syntaxList.getChildren().forEach((node) => { - if (Node.isInterfaceDeclaration(node)) { - statements.push(this.transformInterface(node, delegateInfo)); - } else if (Node.isTypeAliasDeclaration(node)) { - statements.push(this.transformTypeAlias(node, delegateInfo)); - } else { - statements.push(node.getText()); - } - }); - - const structure = sf.getStructure(); - structure.statements = statements; - - const sfNew = project.createSourceFile(`${prismaClientDir}/models/${d.name}-fixed.ts`, structure, { - overwrite: true, - }); - await sfNew.save(); - } - - for (const d of this.model.declarations.filter(isDataModel)) { - const fixedFileName = `${prismaClientDir}/models/${d.name}-fixed.ts`; - const fileName = `${prismaClientDir}/models/${d.name}.ts`; - - fs.renameSync(fixedFileName, fileName); - } - } - - private transformPrismaTypes(sf: SourceFile, sfNew: SourceFile, delegateInfo: DelegateInfo) { - // copy toplevel imports - sfNew.addImportDeclarations(sf.getImportDeclarations().map((n) => n.getStructure())); - - // copy toplevel import equals - sfNew.addStatements(sf.getChildrenOfKind(SyntaxKind.ImportEqualsDeclaration).map((n) => n.getFullText())); - - // copy toplevel exports - sfNew.addExportAssignments(sf.getExportAssignments().map((n) => n.getStructure())); - - // copy toplevel type aliases - sfNew.addTypeAliases(sf.getTypeAliases().map((n) => n.getStructure())); - - // copy toplevel classes - sfNew.addClasses(sf.getClasses().map((n) => n.getStructure())); - - // copy toplevel variables - sfNew.addVariableStatements(sf.getVariableStatements().map((n) => n.getStructure())); - - // copy toplevel namespaces except for `Prisma` - sfNew.addModules( - sf - .getModules() - .filter((n) => n.getName() !== 'Prisma') - .map((n) => n.getStructure()) - ); - - // transform the `Prisma` namespace - const prismaModule = sf.getModuleOrThrow('Prisma'); - const newPrismaModule = sfNew.addModule({ name: 'Prisma', isExported: true }); - this.transformPrismaModule(prismaModule, newPrismaModule, delegateInfo); - } - - private transformPrismaModule( - prismaModule: ModuleDeclaration, - newPrismaModule: ModuleDeclaration, - delegateInfo: DelegateInfo - ) { - // module block is the direct container of declarations inside a namespace - const moduleBlock = prismaModule.getFirstChildByKindOrThrow(SyntaxKind.ModuleBlock); - - // most of the toplevel constructs should be copied over - // here we use ts-morph batch operations for optimal performance - - // copy imports - newPrismaModule.addStatements( - moduleBlock.getChildrenOfKind(SyntaxKind.ImportEqualsDeclaration).map((n) => n.getFullText()) - ); - - // copy classes - newPrismaModule.addClasses(moduleBlock.getClasses().map((n) => n.getStructure())); - - // copy functions - newPrismaModule.addFunctions( - moduleBlock.getFunctions().map((n) => n.getStructure() as FunctionDeclarationStructure) - ); - - // copy nested namespaces - newPrismaModule.addModules(moduleBlock.getModules().map((n) => n.getStructure())); - - // transform variables - const newVariables = moduleBlock - .getVariableStatements() - .map((variable) => this.transformVariableStatement(variable)); - newPrismaModule.addVariableStatements(newVariables); - - // transform interfaces - const newInterfaces = moduleBlock.getInterfaces().map((iface) => this.transformInterface(iface, delegateInfo)); - newPrismaModule.addInterfaces(newInterfaces); - - // transform type aliases - const newTypeAliases = moduleBlock - .getTypeAliases() - .map((typeAlias) => this.transformTypeAlias(typeAlias, delegateInfo)); - newPrismaModule.addTypeAliases(newTypeAliases); - } - - private transformVariableStatement(variable: VariableStatement) { - const structure = variable.getStructure(); - - // remove `delegate_aux_*` fields from the variable's typing - const auxFields = this.findAuxDecls(variable); - if (auxFields.length > 0) { - structure.declarations.forEach((variable) => { - if (variable.type) { - let source = variable.type.toString(); - auxFields.forEach((f) => { - source = this.removeFromSource(source, f.getText()); - }); - variable.type = source; - } - }); - } - - return structure; - } - - private transformVariableStatementProps(variable: VariableStatement) { - const structure = variable.getStructure(); - - // remove `delegate_aux_*` fields from the variable's initializer - const auxFields = this.findAuxProps(variable); - if (auxFields.length > 0) { - structure.declarations.forEach((variable) => { - if (variable.initializer) { - let source = variable.initializer; - auxFields.forEach((f) => { - invariant(typeof source === 'string'); - source = this.removeFromSource(source, f.getText()); - }); - variable.initializer = source; - } - }); - } - - return structure; - } - - private transformInterface(iface: InterfaceDeclaration, delegateInfo: DelegateInfo) { - const structure = iface.getStructure(); - - // filter out aux fields - structure.properties = structure.properties?.filter((p) => !p.name.includes(DELEGATE_AUX_RELATION_PREFIX)); - - // filter out aux methods - structure.methods = structure.methods?.filter((m) => !m.name.includes(DELEGATE_AUX_RELATION_PREFIX)); - - if (delegateInfo.some(([delegate]) => `${delegate.name}Delegate` === iface.getName())) { - // delegate models cannot be created directly, remove create/createMany/upsert - structure.methods = structure.methods?.filter( - (m) => !['create', 'createMany', 'createManyAndReturn', 'upsert'].includes(m.name) - ); - } - - return structure; - } - - private transformTypeAlias(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo) { - const structure = typeAlias.getStructure(); - let source = structure.type as string; - - // remove aux fields - source = this.removeAuxFieldsFromTypeAlias(typeAlias, source); - - // remove discriminator field from concrete input types - source = this.removeDiscriminatorFromConcreteInput(typeAlias, delegateInfo, source); - - // remove create/connectOrCreate/upsert fields from delegate's input types - source = this.removeCreateFromDelegateInput(typeAlias, delegateInfo, source); - - // remove delegate fields from nested mutation input types - source = this.removeDelegateFieldsFromNestedMutationInput(typeAlias, delegateInfo, source); - - // fix delegate payload union type - source = this.fixDelegatePayloadType(typeAlias, delegateInfo, source); - - // fix fk and relation fields related to using `auth()` in `@default` - source = this.fixDefaultAuthType(typeAlias, source); - - // fix json field type - source = this.fixJsonFieldType(typeAlias, source); - - structure.type = source; - return structure; - } - - private fixDelegatePayloadType(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo, source: string) { - // change the type of `$Payload` type of delegate model to a union of concrete types - const typeName = typeAlias.getName(); - const payloadRecord = delegateInfo.find(([delegate]) => `$${delegate.name}Payload` === typeName); - if (payloadRecord) { - const discriminatorDecl = getDiscriminatorField(payloadRecord[0]); - if (discriminatorDecl) { - source = `${payloadRecord[1] - .map( - (concrete) => - `(Prisma.$${concrete.name}Payload & { scalars: { ${discriminatorDecl.name}: '${concrete.name}' } })` - ) - .join(' | ')}`; - } - } - return source; - } - - private removeCreateFromDelegateInput(typeAlias: TypeAliasDeclaration, delegateInfo: DelegateInfo, source: string) { - // remove create/connectOrCreate/upsert fields from delegate's input types because - // delegate models cannot be created directly - const typeName = typeAlias.getName(); - const delegateModelNames = delegateInfo.map(([delegate]) => delegate.name); - const delegateCreateUpdateInputRegex = new RegExp( - `^(${delegateModelNames.join('|')})(Unchecked)?(Create|Update).*Input$` - ); - if (delegateCreateUpdateInputRegex.test(typeName)) { - const toRemove = typeAlias - .getDescendantsOfKind(SyntaxKind.PropertySignature) - .filter((p) => ['create', 'createMany', 'connectOrCreate', 'upsert'].includes(p.getName())); - toRemove.forEach((r) => { - this.removeFromSource(source, r.getText()); - }); - } - return source; - } - - private removeDiscriminatorFromConcreteInput( - typeAlias: TypeAliasDeclaration, - delegateInfo: DelegateInfo, - source: string - ) { - // remove discriminator field from the create/update input because discriminator cannot be set directly - const typeName = typeAlias.getName(); - - const delegateModelNames = delegateInfo.map(([delegate]) => delegate.name); - const concreteModelNames = delegateInfo - .map(([_, concretes]) => concretes.flatMap((c) => c.name)) - .flatMap((name) => name); - const allModelNames = [...new Set([...delegateModelNames, ...concreteModelNames])]; - const concreteCreateUpdateInputRegex = new RegExp( - `^(${allModelNames.join('|')})(Unchecked)?(Create|Update).*Input$` - ); - - const match = typeName.match(concreteCreateUpdateInputRegex); - if (match) { - const modelName = this.resolveName(match[1]); - const dataModel = this.model.declarations.find( - (d): d is DataModel => isDataModel(d) && d.name === modelName - ); - - if (!dataModel) { - return source; - } - - for (const field of dataModel.fields) { - if (isDiscriminatorField(field)) { - const fieldDef = this.findNamedProperty(typeAlias, field.name); - if (fieldDef) { - source = this.removeFromSource(source, fieldDef.getText()); - } - } - } - } - return source; - } - - private removeAuxFieldsFromTypeAlias(typeAlias: TypeAliasDeclaration, source: string) { - // remove `delegate_aux_*` fields from the type alias - const auxDecls = this.findAuxDecls(typeAlias); - if (auxDecls.length > 0) { - auxDecls.forEach((d) => { - source = this.removeFromSource(source, d.getText()); - }); - } - return source; - } - - private readonly CreateUpdateWithoutDelegateRelationRegex = new RegExp( - `(.+)(Create|Update)Without${upperCaseFirst(DELEGATE_AUX_RELATION_PREFIX)}_(.+)Input` - ); - - private removeDelegateFieldsFromNestedMutationInput( - typeAlias: TypeAliasDeclaration, - _delegateInfo: DelegateInfo, - source: string - ) { - const name = typeAlias.getName(); - - // remove delegate model fields (and corresponding fk fields) from - // create/update input types nested inside concrete models - - const match = name.match(this.CreateUpdateWithoutDelegateRelationRegex); - if (!match) { - return source; - } - - // [modelName]_[relationFieldName]_[concreteModelName] - const nameTuple = this.resolveName(match[3], true); - const [modelName, relationFieldName, _] = nameTuple!.split('_'); - - const fieldDef = this.findNamedProperty(typeAlias, relationFieldName); - if (fieldDef) { - // remove relation field of delegate type, e.g., `asset` - source = this.removeFromSource(source, fieldDef.getText()); - } - - // remove fk fields related to the delegate type relation, e.g., `assetId` - - const relationModel = this.model.declarations.find( - (d): d is DataModel => isDataModel(d) && d.name === modelName - ); - - if (!relationModel) { - return source; - } - - const relationField = relationModel.fields.find((f) => f.name === relationFieldName); - if (!relationField) { - return source; - } - - const relAttr = getAttribute(relationField, '@relation'); - if (!relAttr) { - return source; - } - - const fieldsArg = getAttributeArg(relAttr, 'fields'); - let fkFields: string[] = []; - if (isArrayExpr(fieldsArg)) { - fkFields = fieldsArg.items.map((e) => (e as ReferenceExpr).target.$refText); - } - - fkFields.forEach((fkField) => { - const fieldDef = this.findNamedProperty(typeAlias, fkField); - if (fieldDef) { - source = this.removeFromSource(source, fieldDef.getText()); - } - }); - - return source; - } - - // resolves a potentially shortened name back to the original - private resolveName(name: string | undefined, withDelegateAuxPrefix = false) { - if (!name) { - return name; - } - const shortNameLookupKey = withDelegateAuxPrefix ? `${DELEGATE_AUX_RELATION_PREFIX}_${name}` : name; - if (this.reversedShortNameMap.has(shortNameLookupKey)) { - name = this.reversedShortNameMap.get(shortNameLookupKey)!; - if (withDelegateAuxPrefix) { - name = name.substring(DELEGATE_AUX_RELATION_PREFIX.length + 1); - } - } - return name; - } - - private fixDefaultAuthType(typeAlias: TypeAliasDeclaration, source: string) { - const match = typeAlias.getName().match(this.modelsWithAuthInDefaultCreateInputPattern); - if (!match) { - return source; - } - - const modelName = this.resolveName(match[1]); - const dataModel = this.model.declarations.find((d): d is DataModel => isDataModel(d) && d.name === modelName); - if (dataModel) { - for (const fkField of dataModel.fields.filter((f) => f.attributes.some(isDefaultWithAuth))) { - // change fk field to optional since it has a default - source = source.replace(new RegExp(`^(\\s*${fkField.name}\\s*):`, 'm'), `$1?:`); - - const relationField = getRelationField(fkField); - if (relationField) { - // change relation field to optional since its fk has a default - source = source.replace(new RegExp(`^(\\s*${relationField.name}\\s*):`, 'm'), `$1?:`); - } - } - } - return source; - } - - private fixJsonFieldType(typeAlias: TypeAliasDeclaration, source: string) { - const typeName = typeAlias.getName(); - - const getTypedJsonFields = (model: DataModel) => { - return model.fields.filter((f) => isTypeDef(f.type.reference?.ref)); - }; - - const replacePrismaJson = (source: string, field: DataModelField) => { - let replaceValue = `$1: ${field.type.reference!.$refText}`; - if (field.type.array) { - replaceValue += '[]'; - } - if (field.type.optional) { - replaceValue += ' | null'; - } - - // Check if the field in the source is optional (has a `?`) - const isOptionalInSource = new RegExp(`(${field.name}\\?\\s*):`).test(source); - if (isOptionalInSource) { - replaceValue += ' | runtime.Types.Skip'; - } - - return source.replace(new RegExp(`(${field.name}\\??\\s*):[^\\n]+`), replaceValue); - }; - - // fix "$[Model]Payload" type - const payloadModelMatch = this.modelsWithJsonTypeFields.find((m) => `$${m.name}Payload` === typeName); - if (payloadModelMatch) { - const scalars = typeAlias - .getDescendantsOfKind(SyntaxKind.PropertySignature) - .find((p) => p.getName() === 'scalars'); - if (!scalars) { - return source; - } - - const fieldsToFix = getTypedJsonFields(payloadModelMatch); - for (const field of fieldsToFix) { - source = replacePrismaJson(source, field); - } - } - - // fix input/output types, "[Model]CreateInput", etc. - for (const pattern of this.modelsWithJsonTypeFieldsInputOutputPattern) { - const match = typeName.match(pattern); - if (!match) { - continue; - } - // first capture group is the model name - const modelName = this.resolveName(match[1]); - const model = this.modelsWithJsonTypeFields.find((m) => m.name === modelName); - const fieldsToFix = getTypedJsonFields(model!); - for (const field of fieldsToFix) { - source = replacePrismaJson(source, field); - } - break; - } - - return source; - } - - private generateExtraTypes(sf: SourceFile) { - for (const decl of this.model.declarations) { - if (isTypeDef(decl)) { - generateTypeDefType(sf, decl); - } - } - } - - private findNamedProperty(typeAlias: TypeAliasDeclaration, name: string) { - return typeAlias.getFirstDescendant((d) => d.isKind(SyntaxKind.PropertySignature) && d.getName() === name); - } - - private findAuxDecls(node: Node) { - return node - .getDescendantsOfKind(SyntaxKind.PropertySignature) - .filter((n) => n.getName().includes(DELEGATE_AUX_RELATION_PREFIX)); - } - - private findAuxProps(node: Node) { - return node - .getDescendantsOfKind(SyntaxKind.PropertyAssignment) - .filter((n) => n.getName().includes(DELEGATE_AUX_RELATION_PREFIX)); - } - - private saveSourceFile(sf: SourceFile) { - if (this.options.preserveTsFiles) { - saveSourceFile(sf); - } - } - - private get generatePermissionChecker() { - return this.options.generatePermissionChecker === true; - } - - private removeFromSource(source: string, text: string) { - source = source.replace(text, ''); - return this.trimEmptyLines(source); - } - - private trimEmptyLines(source: string): string { - return source.replace(/^\s*,?[\r\n]/gm, ''); - } - - private get isNewPrismaClientGenerator() { - const gen = getPrismaClientGenerator(this.model); - return !!gen?.isNewGenerator; - } -} diff --git a/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts b/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts deleted file mode 100644 index 00f16095f..000000000 --- a/packages/schema/src/plugins/enhancer/enhance/model-typedef-generator.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { getDataModels, PluginError } from '@zenstackhq/sdk'; -import { BuiltinType, Enum, isEnum, TypeDef, TypeDefFieldType } from '@zenstackhq/sdk/ast'; -import { SourceFile } from 'ts-morph'; -import { match } from 'ts-pattern'; -import { name } from '..'; - -export function generateTypeDefType(sourceFile: SourceFile, decl: TypeDef) { - sourceFile.addTypeAlias({ - name: decl.name, - isExported: true, - docs: decl.comments.map((c) => unwrapTripleSlashComment(c)), - type: (writer) => { - writer.block(() => { - decl.fields.forEach((field) => { - if (field.comments.length > 0) { - writer.writeLine(` /**`); - field.comments.forEach((c) => writer.writeLine(` * ${unwrapTripleSlashComment(c)}`)); - writer.writeLine(` */`); - } - // optional fields are also nullable (to be consistent with Prisma) - writer.writeLine( - ` ${field.name}${field.type.optional ? '?' : ''}: ${zmodelTypeToTsType(field.type)}${ - field.type.optional ? ' | null' : '' - };` - ); - }); - }); - }, - }); -} - -function unwrapTripleSlashComment(c: string): string { - return c.replace(/^[/]*\s*/, ''); -} - -function zmodelTypeToTsType(type: TypeDefFieldType) { - let result: string; - - if (type.type) { - result = builtinTypeToTsType(type.type); - } else if (type.reference?.ref) { - if (isEnum(type.reference.ref)) { - result = makeEnumTypeReference(type.reference.ref); - } else { - result = type.reference.ref.name; - } - } else { - throw new PluginError(name, `Unsupported field type: ${type}`); - } - - if (type.array) { - result += '[]'; - } - - return result; -} - -function builtinTypeToTsType(type: BuiltinType) { - return match(type) - .with('Boolean', () => 'boolean') - .with('BigInt', () => 'bigint') - .with('Int', () => 'number') - .with('Float', () => 'number') - .with('Decimal', () => 'Prisma.Decimal') - .with('String', () => 'string') - .with('Bytes', () => 'Uint8Array') - .with('DateTime', () => 'Date') - .with('Json', () => 'unknown') - .exhaustive(); -} - -function makeEnumTypeReference(enumDecl: Enum) { - const zmodel = enumDecl.$container; - const models = getDataModels(zmodel); - - if (models.some((model) => model.fields.some((field) => field.type.reference?.ref === enumDecl))) { - // if the enum is referenced by any data model, Prisma already generates its type, - // we just need to reference it - return enumDecl.name; - } else { - // otherwise, we need to inline the enum - return enumDecl.fields.map((field) => `'${field.name}'`).join(' | '); - } -} diff --git a/packages/schema/src/plugins/enhancer/enhancer-utils.ts b/packages/schema/src/plugins/enhancer/enhancer-utils.ts deleted file mode 100644 index 9bb429ca5..000000000 --- a/packages/schema/src/plugins/enhancer/enhancer-utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { isAuthInvocation } from '@zenstackhq/sdk'; -import type { DataModelFieldAttribute } from '@zenstackhq/sdk/ast'; -import { streamAst } from 'langium'; - -/** - * Check if the given field attribute is a `@default` with `auth()` invocation - */ -export function isDefaultWithAuth(attr: DataModelFieldAttribute) { - if (attr.decl.ref?.name !== '@default') { - return false; - } - - const expr = attr.args[0]?.value; - if (!expr) { - return false; - } - - // find `auth()` in default value expression - return streamAst(expr).some(isAuthInvocation); -} diff --git a/packages/schema/src/plugins/enhancer/index.ts b/packages/schema/src/plugins/enhancer/index.ts deleted file mode 100644 index e81d2b987..000000000 --- a/packages/schema/src/plugins/enhancer/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - PluginError, - RUNTIME_PACKAGE, - createProject, - getPrismaClientGenerator, - normalizedRelative, - resolvePath, - type PluginFunction, -} from '@zenstackhq/sdk'; -import path from 'path'; -import { getDefaultOutputFolder } from '../plugin-utils'; -import { EnhancerGenerator } from './enhance'; -import { generate as generateModelMeta } from './model-meta'; -import { generate as generatePolicy } from './policy'; - -export const name = 'Prisma Enhancer'; -export const description = 'Generating PrismaClient enhancer'; - -const run: PluginFunction = async (model, options, _dmmf, globalOptions) => { - let outDir = options.output ? (options.output as string) : getDefaultOutputFolder(globalOptions); - if (!outDir) { - throw new PluginError(name, `Unable to determine output path, not running plugin`); - } - outDir = resolvePath(outDir, options); - - const project = globalOptions?.tsProject ?? createProject(); - - await generateModelMeta(model, options, project, outDir); - await generatePolicy(model, options, project, outDir); - const { dmmf, newPrismaClientDtsPath } = await new EnhancerGenerator(model, options, project, outDir).generate(); - - let prismaClientPath: string | undefined; - if (dmmf) { - // a logical client is generated - if (options.output || globalOptions?.output) { - // handle custom output path - - // get the absolute path of the prisma client types - const prismaGenerator = getPrismaClientGenerator(model); - const prismaClientPathAbs = path.resolve(outDir, prismaGenerator?.isNewGenerator ? 'client' : 'models'); - - // resolve it relative to the schema path - prismaClientPath = normalizedRelative(path.dirname(options.schemaPath), prismaClientPathAbs); - } else { - prismaClientPath = `${RUNTIME_PACKAGE}/models`; - } - } - - return { dmmf, warnings: [], prismaClientPath, prismaClientDtsPath: newPrismaClientDtsPath }; -}; - -export default run; diff --git a/packages/schema/src/plugins/enhancer/model-meta/index.ts b/packages/schema/src/plugins/enhancer/model-meta/index.ts deleted file mode 100644 index 53fecdb82..000000000 --- a/packages/schema/src/plugins/enhancer/model-meta/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { generateModelMeta, getDataModels, type PluginOptions } from '@zenstackhq/sdk'; -import { isTypeDef, type Model } from '@zenstackhq/sdk/ast'; -import path from 'path'; -import type { Project } from 'ts-morph'; - -export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { - const outFile = path.join(outDir, 'model-meta.ts'); - const dataModels = getDataModels(model); - const typeDefs = model.declarations.filter(isTypeDef); - - // save ts files if requested explicitly or the user provided - const preserveTsFiles = options.preserveTsFiles === true || !!options.output; - await generateModelMeta(project, dataModels, typeDefs, { - output: outFile, - generateAttributes: true, - preserveTsFiles, - shortNameMap: options.shortNameMap, - }); -} diff --git a/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts b/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts deleted file mode 100644 index 674348470..000000000 --- a/packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts +++ /dev/null @@ -1,411 +0,0 @@ -import { - PluginError, - getLiteral, - getRelationKeyPairs, - isAuthInvocation, - isDataModelFieldReference, - isEnumFieldReference, -} from '@zenstackhq/sdk'; -import { - BinaryExpr, - BooleanLiteral, - DataModel, - DataModelField, - Expression, - ExpressionType, - InvocationExpr, - LiteralExpr, - MemberAccessExpr, - NumberLiteral, - ReferenceExpr, - StringLiteral, - UnaryExpr, - isBinaryExpr, - isDataModelField, - isEnum, - isLiteralExpr, - isMemberAccessExpr, - isNullExpr, - isReferenceExpr, - isThisExpr, - isUnaryExpr, -} from '@zenstackhq/sdk/ast'; -import { P, match } from 'ts-pattern'; -import { name } from '..'; -import { isCheckInvocation } from '../../../utils/ast-utils'; - -/** - * Options for {@link ConstraintTransformer}. - */ -export type ConstraintTransformerOptions = { - authAccessor: string; -}; - -/** - * Transform a set of allow and deny rules into a single constraint expression. - */ -export class ConstraintTransformer { - // a counter for generating unique variable names - private varCounter = 0; - - constructor(private readonly options: ConstraintTransformerOptions) {} - - /** - * Transforms a set of allow and deny rules into a single constraint expression. - */ - transformRules(allows: Expression[], denies: Expression[]): string { - // reset state - this.varCounter = 0; - - if (allows.length === 0) { - // unconditionally deny - return this.value('false', 'boolean'); - } - - let result: string; - - // transform allow rules - const allowConstraints = allows.map((allow) => this.transformExpression(allow)); - if (allowConstraints.length > 1) { - result = this.or(...allowConstraints); - } else { - result = allowConstraints[0]; - } - - // transform deny rules and compose - if (denies.length > 0) { - const denyConstraints = denies.map((deny) => this.transformExpression(deny)); - result = this.and(result, ...denyConstraints.map((c) => this.not(c))); - } - - // DEBUG: - // console.log(`Constraint transformation result:\n${JSON.stringify(result, null, 2)}`); - - return result; - } - - private and(...constraints: string[]) { - if (constraints.length === 0) { - throw new Error('No expressions to combine'); - } - return constraints.length === 1 ? constraints[0] : `{ kind: 'and', children: [ ${constraints.join(', ')} ] }`; - } - - private or(...constraints: string[]) { - if (constraints.length === 0) { - throw new Error('No expressions to combine'); - } - return constraints.length === 1 ? constraints[0] : `{ kind: 'or', children: [ ${constraints.join(', ')} ] }`; - } - - private not(constraint: string) { - return `{ kind: 'not', children: [${constraint}] }`; - } - - private transformExpression(expression: Expression) { - return ( - match(expression) - .when(isBinaryExpr, (expr) => this.transformBinary(expr)) - .when(isUnaryExpr, (expr) => this.transformUnary(expr)) - // top-level boolean literal - .when(isLiteralExpr, (expr) => this.transformLiteral(expr)) - // top-level boolean reference expr - .when(isReferenceExpr, (expr) => this.transformReference(expr)) - // top-level boolean member access expr - .when(isMemberAccessExpr, (expr) => this.transformMemberAccess(expr)) - // `check()` invocation on a relation field - .when(isCheckInvocation, (expr) => this.transformCheckInvocation(expr as InvocationExpr)) - .otherwise(() => this.nextVar()) - ); - } - - private transformLiteral(expr: LiteralExpr) { - return match(expr.$type) - .with(NumberLiteral, () => { - const parsed = parseFloat(expr.value as string); - if (isNaN(parsed) || parsed < 0 || !Number.isInteger(parsed)) { - // only non-negative integers are supported, for other cases, - // transform into a free variable - return this.nextVar('number'); - } - return this.value(expr.value.toString(), 'number'); - }) - .with(StringLiteral, () => this.value(`'${expr.value}'`, 'string')) - .with(BooleanLiteral, () => this.value(expr.value.toString(), 'boolean')) - .exhaustive(); - } - - private transformReference(expr: ReferenceExpr) { - // top-level reference is transformed into a named variable - return this.variable(expr.target.$refText, 'boolean'); - } - - private transformMemberAccess(expr: MemberAccessExpr) { - // "this.x" is transformed into a named variable - if (isThisExpr(expr.operand)) { - return this.variable(expr.member.$refText, 'boolean'); - } - - // top-level auth access - const authAccess = this.getAuthAccess(expr); - if (authAccess) { - return this.value(`${authAccess} ?? false`, 'boolean'); - } - - // other top-level member access expressions are not supported - // and thus transformed into a free variable - return this.nextVar(); - } - - private transformBinary(expr: BinaryExpr): string { - return ( - match(expr.operator) - .with('&&', () => this.and(this.transformExpression(expr.left), this.transformExpression(expr.right))) - .with('||', () => this.or(this.transformExpression(expr.left), this.transformExpression(expr.right))) - .with(P.union('==', '!=', '<', '<=', '>', '>='), () => this.transformComparison(expr)) - // unsupported operators (e.g., collection predicate) are transformed into a free variable - .otherwise(() => this.nextVar()) - ); - } - - private transformUnary(expr: UnaryExpr): string { - return match(expr.operator) - .with('!', () => this.not(this.transformExpression(expr.operand))) - .exhaustive(); - } - - private transformComparison(expr: BinaryExpr) { - if (isAuthInvocation(expr.left) || isAuthInvocation(expr.right)) { - // handle the case if any operand is `auth()` invocation - const authComparison = this.transformAuthComparison(expr); - return authComparison ?? this.nextVar(); - } - - const leftOperand = this.getComparisonOperand(expr.left); - const rightOperand = this.getComparisonOperand(expr.right); - - const op = this.mapOperatorToConstraintKind(expr.operator); - const result = `{ kind: '${op}', left: ${leftOperand}, right: ${rightOperand} }`; - - // `auth()` member access can be undefined, when that happens, we assume a false condition - // for the comparison - - const leftAuthAccess = this.getAuthAccess(expr.left); - const rightAuthAccess = this.getAuthAccess(expr.right); - - if (leftAuthAccess && rightOperand) { - // `auth().f op x` => `auth().f !== undefined && auth().f op x` - return this.and(this.value(`${this.normalizeToNull(leftAuthAccess)} !== null`, 'boolean'), result); - } else if (rightAuthAccess && leftOperand) { - // `x op auth().f` => `auth().f !== undefined && x op auth().f` - return this.and(this.value(`${this.normalizeToNull(rightAuthAccess)} !== null`, 'boolean'), result); - } - - if (leftOperand === undefined || rightOperand === undefined) { - // if either operand is not supported, transform into a free variable - return this.nextVar(); - } - - return result; - } - - private transformAuthComparison(expr: BinaryExpr) { - if (this.isAuthEqualNull(expr)) { - // `auth() == null` => `user === null` - return this.value(`${this.options.authAccessor} === null`, 'boolean'); - } - - if (this.isAuthNotEqualNull(expr)) { - // `auth() != null` => `user !== null` - return this.value(`${this.options.authAccessor} !== null`, 'boolean'); - } - - // auth() equality check against a relation, translate to id-fk comparison - const operand = isAuthInvocation(expr.left) ? expr.right : expr.left; - if (!isDataModelFieldReference(operand)) { - return undefined; - } - - // get id-fk field pairs from the relation field - const relationField = operand.target.ref as DataModelField; - const idFkPairs = getRelationKeyPairs(relationField); - - // build id-fk field comparison constraints - const fieldConstraints: string[] = []; - - idFkPairs.forEach(({ id, foreignKey }) => { - const idFieldType = this.mapType(id.type.type as ExpressionType); - if (!idFieldType) { - return; - } - const fkFieldType = this.mapType(foreignKey.type.type as ExpressionType); - if (!fkFieldType) { - return; - } - - const op = this.mapOperatorToConstraintKind(expr.operator); - const authIdAccess = `${this.options.authAccessor}?.${id.name}`; - - fieldConstraints.push( - this.and( - // `auth()?.id != null` guard - this.value(`${this.normalizeToNull(authIdAccess)} !== null`, 'boolean'), - // `auth()?.id [op] fkField` - `{ kind: '${op}', left: ${this.value(authIdAccess, idFieldType)}, right: ${this.variable( - foreignKey.name, - fkFieldType - )} }` - ) - ); - }); - - // combine field constraints - if (fieldConstraints.length > 0) { - return this.and(...fieldConstraints); - } - - return undefined; - } - - private transformCheckInvocation(expr: InvocationExpr) { - // transform `check()` invocation to a special "delegate" constraint kind - // to be evaluated at runtime - - const field = expr.args[0].value as ReferenceExpr; - if (!field) { - throw new PluginError(name, 'Invalid check invocation'); - } - const fieldType = field.$resolvedType?.decl as DataModel; - - let operation: string | undefined = undefined; - if (expr.args[1]) { - operation = getLiteral(expr.args[1].value); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result: any = { kind: 'delegate', model: fieldType.name, relation: field.target.$refText }; - if (operation) { - // operation can be explicitly specified or inferred from the context - result.operation = operation; - } - return JSON.stringify(result); - } - - // normalize `auth()` access undefined value to null - private normalizeToNull(expr: string) { - return `(${expr} ?? null)`; - } - - private isAuthEqualNull(expr: BinaryExpr) { - return ( - expr.operator === '==' && - ((isAuthInvocation(expr.left) && isNullExpr(expr.right)) || - (isAuthInvocation(expr.right) && isNullExpr(expr.left))) - ); - } - - private isAuthNotEqualNull(expr: BinaryExpr) { - return ( - expr.operator === '!=' && - ((isAuthInvocation(expr.left) && isNullExpr(expr.right)) || - (isAuthInvocation(expr.right) && isNullExpr(expr.left))) - ); - } - - private getComparisonOperand(expr: Expression) { - if (isLiteralExpr(expr)) { - return this.transformLiteral(expr); - } - - if (isEnumFieldReference(expr)) { - return this.value(`'${expr.target.$refText}'`, 'string'); - } - - const fieldAccess = this.getFieldAccess(expr); - if (fieldAccess) { - // model field access is transformed into a named variable - const mappedType = this.mapExpressionType(expr); - if (mappedType) { - return this.variable(fieldAccess.name, mappedType); - } else { - return undefined; - } - } - - const authAccess = this.getAuthAccess(expr); - if (authAccess) { - const mappedType = this.mapExpressionType(expr); - if (mappedType) { - return `${this.value(authAccess, mappedType)}`; - } else { - return undefined; - } - } - - return undefined; - } - - private mapExpressionType(expression: Expression) { - if (isEnum(expression.$resolvedType?.decl)) { - return 'string'; - } else { - return this.mapType(expression.$resolvedType?.decl as ExpressionType); - } - } - - private mapType(type: ExpressionType) { - return match(type) - .with('Boolean', () => 'boolean') - .with('Int', () => 'number') - .with('String', () => 'string') - .otherwise(() => undefined); - } - - private mapOperatorToConstraintKind(operator: BinaryExpr['operator']) { - return match(operator) - .with('==', () => 'eq') - .with('!=', () => 'ne') - .with('<', () => 'lt') - .with('<=', () => 'lte') - .with('>', () => 'gt') - .with('>=', () => 'gte') - .otherwise(() => { - throw new Error(`Unsupported operator: ${operator}`); - }); - } - - private getFieldAccess(expr: Expression) { - if (isReferenceExpr(expr)) { - return isDataModelField(expr.target.ref) ? { name: expr.target.$refText } : undefined; - } - if (isMemberAccessExpr(expr)) { - return isThisExpr(expr.operand) ? { name: expr.member.$refText } : undefined; - } - return undefined; - } - - private getAuthAccess(expr: Expression): string | undefined { - if (!isMemberAccessExpr(expr)) { - return undefined; - } - - if (isAuthInvocation(expr.operand)) { - return `${this.options.authAccessor}?.${expr.member.$refText}`; - } else { - const operand = this.getAuthAccess(expr.operand); - return operand ? `${operand}?.${expr.member.$refText}` : undefined; - } - } - - private nextVar(type = 'boolean') { - return this.variable(`__var${this.varCounter++}`, type); - } - - private variable(name: string, type: string) { - return `{ kind: 'variable', name: '${name}', type: '${type}' }`; - } - - private value(value: string, type: string) { - return `{ kind: 'value', value: ${value}, type: '${type}' }`; - } -} diff --git a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts b/packages/schema/src/plugins/enhancer/policy/expression-writer.ts deleted file mode 100644 index d3dccfaa6..000000000 --- a/packages/schema/src/plugins/enhancer/policy/expression-writer.ts +++ /dev/null @@ -1,855 +0,0 @@ -import { - BinaryExpr, - BooleanLiteral, - DataModel, - DataModelField, - Expression, - InvocationExpr, - isDataModel, - isDataModelField, - isEnumField, - isMemberAccessExpr, - isReferenceExpr, - isThisExpr, - LiteralExpr, - MemberAccessExpr, - NumberLiteral, - ReferenceExpr, - ReferenceTarget, - StringLiteral, - UnaryExpr, -} from '@zenstackhq/language/ast'; -import { DELEGATE_AUX_RELATION_PREFIX, PolicyOperationKind } from '@zenstackhq/runtime'; -import { - CodeWriter, - ExpressionContext, - getFunctionExpressionContext, - getIdFields, - getLiteral, - getQueryGuardFunctionName, - isAuthInvocation, - isDataModelFieldReference, - isDelegateModel, - isFromStdlib, - isFutureExpr, - PluginError, - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, -} from '@zenstackhq/sdk'; -import { lowerCaseFirst, invariant } from '@zenstackhq/runtime/local-helpers'; -import { name } from '..'; -import { isCheckInvocation } from '../../../utils/ast-utils'; - -type ComparisonOperator = '==' | '!=' | '>' | '>=' | '<' | '<='; - -type FilterOperators = - | 'is' - | 'some' - | 'every' - | 'none' - | 'in' - | 'contains' - | 'search' - | 'startsWith' - | 'endsWith' - | 'has' - | 'hasEvery' - | 'hasSome' - | 'isEmpty'; - -// { OR: [] } filters to nothing, { AND: [] } includes everything -// https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals -export const TRUE = '{ AND: [] }'; -export const FALSE = '{ OR: [] }'; - -export type ExpressionWriterOptions = { - isPostGuard?: boolean; - operationContext: PolicyOperationKind; -}; - -/** - * Utility for writing ZModel expression as Prisma query argument objects into a ts-morph writer - */ -export class ExpressionWriter { - private readonly plainExprBuilder: TypeScriptExpressionTransformer; - - /** - * Constructs a new ExpressionWriter - */ - constructor(private readonly writer: CodeWriter, private readonly options: ExpressionWriterOptions) { - this.plainExprBuilder = new TypeScriptExpressionTransformer({ - context: ExpressionContext.AccessPolicy, - isPostGuard: this.options.isPostGuard, - // in post-guard context, `this` references pre-update value - thisExprContext: this.options.isPostGuard ? 'context.preValue' : undefined, - operationContext: this.options.operationContext, - }); - } - - /** - * Writes the given ZModel expression. - */ - write(expr: Expression): void { - switch (expr.$type) { - case StringLiteral: - case NumberLiteral: - case BooleanLiteral: - this.writeLiteral(expr as LiteralExpr); - break; - - case UnaryExpr: - this.writeUnary(expr as UnaryExpr); - break; - - case BinaryExpr: - this.writeBinary(expr as BinaryExpr); - break; - - case ReferenceExpr: - this.writeReference(expr as ReferenceExpr); - break; - - case MemberAccessExpr: - this.writeMemberAccess(expr as MemberAccessExpr); - break; - - case InvocationExpr: - this.writeInvocation(expr as InvocationExpr); - break; - - default: - throw new Error(`Not implemented: ${expr.$type}`); - } - } - - private writeReference(expr: ReferenceExpr) { - if (isEnumField(expr.target.ref)) { - throw new Error('We should never get here'); - } else { - this.block(() => { - const ref = expr.target.ref; - invariant(ref); - if (this.isFieldReferenceToDelegateModel(ref)) { - const thisModel = ref.$container as DataModel; - const targetBase = ref.$inheritedFrom; - this.writeBaseHierarchy(thisModel, targetBase, () => this.writer.write(`${ref.name}: true`)); - } else { - this.writer.write(`${ref.name}: true`); - } - }); - } - } - - private writeBaseHierarchy(thisModel: DataModel, targetBase: DataModel | undefined, conditionWriter: () => void) { - if (!targetBase || thisModel === targetBase) { - conditionWriter(); - return; - } - - const base = this.getDelegateBase(thisModel); - if (!base) { - throw new PluginError(name, `Failed to resolve delegate base model for "${thisModel.name}"`); - } - - this.writer.write(`${`${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(base.name)}`}: `); - this.writer.block(() => { - this.writeBaseHierarchy(base, targetBase, conditionWriter); - }); - } - - private getDelegateBase(model: DataModel) { - return model.superTypes.map((t) => t.ref).filter((t) => t && isDelegateModel(t))?.[0]; - } - - private isFieldReferenceToDelegateModel(ref: ReferenceTarget): ref is DataModelField { - return isDataModelField(ref) && !!ref.$inheritedFrom && isDelegateModel(ref.$inheritedFrom); - } - - private writeMemberAccess(expr: MemberAccessExpr) { - if (this.isAuthOrAuthMemberAccess(expr)) { - // member access of `auth()`, generate plain expression - this.guard(() => this.plain(expr), true); - } else { - this.block(() => { - // must be a boolean member - this.writeFieldCondition(expr.operand, () => { - this.block(() => { - this.writer.write(`${expr.member.ref?.name}: true`); - }); - }); - }); - } - } - - private writeExprList(exprs: Expression[]) { - this.writer.write('['); - for (let i = 0; i < exprs.length; i++) { - this.write(exprs[i]); - if (i !== exprs.length - 1) { - this.writer.write(','); - } - } - this.writer.write(']'); - } - - private writeBinary(expr: BinaryExpr) { - switch (expr.operator) { - case '&&': - case '||': - this.writeLogical(expr, expr.operator); - break; - - case '==': - case '!=': - case '>': - case '>=': - case '<': - case '<=': - this.writeComparison(expr, expr.operator); - break; - - case 'in': - this.writeIn(expr); - break; - - case '?': - case '!': - case '^': - this.writeCollectionPredicate(expr, expr.operator); - break; - } - } - - private writeIn(expr: BinaryExpr) { - const leftIsFieldAccess = this.isFieldAccess(expr.left); - const rightIsFieldAccess = this.isFieldAccess(expr.right); - - if (!leftIsFieldAccess && !rightIsFieldAccess) { - // 'in' without referencing fields - this.guard(() => this.plain(expr)); - } else { - this.block(() => { - if (leftIsFieldAccess && !rightIsFieldAccess) { - // 'in' with left referencing a field, right is an array literal - this.writeFieldCondition( - expr.left, - () => { - this.plain(expr.right); - }, - 'in' - ); - } else if (!leftIsFieldAccess && rightIsFieldAccess) { - // 'in' with right referencing an array field, left is a literal - // transform it into a 'has' filter - this.writeFieldCondition( - expr.right, - () => { - this.plain(expr.left); - }, - 'has' - ); - } else if ( - isDataModelFieldReference(expr.left) && - isDataModelFieldReference(expr.right) && - expr.left.target.ref?.$container === expr.right.target.ref?.$container - ) { - // comparing two fields of the same model - this.writeFieldCondition( - expr.left, - () => { - this.writeFieldReference(expr.right as ReferenceExpr); - }, - 'in' - ); - } else { - throw new PluginError(name, '"in" operator cannot be used with field references on both sides'); - } - }); - } - } - - private writeCollectionPredicate(expr: BinaryExpr, operator: string) { - // check if the operand should be compiled to a relation query - // or a plain expression - const compileToRelationQuery = - // expression rooted to `auth()` is always compiled to plain expression - !this.isAuthOrAuthMemberAccess(expr.left) && - // `future()` in post-update context - ((this.options.isPostGuard && this.isFutureMemberAccess(expr.left)) || - // non-`future()` in pre-update context - (!this.options.isPostGuard && !this.isFutureMemberAccess(expr.left))); - - if (compileToRelationQuery) { - this.block(() => { - this.writeFieldCondition( - expr.left, - () => { - // inner scope of collection expression is always compiled as non-post-guard - const innerWriter = new ExpressionWriter(this.writer, { - isPostGuard: false, - operationContext: this.options.operationContext, - }); - innerWriter.write(expr.right); - }, - operator === '?' ? 'some' : operator === '!' ? 'every' : 'none' - ); - }); - } else { - const plain = this.plainExprBuilder.transform(expr); - this.writer.write(`${plain} ? ${TRUE} : ${FALSE}`); - } - } - - private isFieldAccess(expr: Expression): boolean { - if (isThisExpr(expr)) { - return true; - } - - if (isMemberAccessExpr(expr)) { - if (isFutureExpr(expr.operand) && this.options.isPostGuard) { - // when writing for post-update, future().field.x is a field access - return true; - } else { - return this.isFieldAccess(expr.operand); - } - } - if (isDataModelFieldReference(expr) && !this.options.isPostGuard) { - return true; - } - return false; - } - - private guard(condition: () => void, cast = false) { - if (cast) { - this.writer.write('!!'); - condition(); - } else { - condition(); - } - this.writer.write(` ? ${TRUE} : ${FALSE}`); - } - - private plain(expr: Expression) { - try { - this.writer.write(this.plainExprBuilder.transform(expr)); - } catch (err) { - if (err instanceof TypeScriptExpressionTransformerError) { - throw new PluginError(name, err.message); - } else { - throw err; - } - } - } - - private writeIdFieldsCheck(model: DataModel, value: Expression) { - const idFields = this.requireIdFields(model); - idFields.forEach((idField, idx) => { - // eg: id: user.id - this.writer.write(`${idField.name}:`); - this.plain(value); - this.writer.write(`.${idField.name}`); - if (idx !== idFields.length - 1) { - this.writer.write(','); - } - }); - } - - private writeComparison(expr: BinaryExpr, operator: ComparisonOperator) { - const leftIsFieldAccess = this.isFieldAccess(expr.left); - const rightIsFieldAccess = this.isFieldAccess(expr.right); - - if (!leftIsFieldAccess && !rightIsFieldAccess) { - // compile down to a plain expression - this.guard(() => { - this.plain(expr); - }); - return; - } - - let fieldAccess: Expression; - let operand: Expression; - if (leftIsFieldAccess) { - fieldAccess = expr.left; - operand = expr.right; - } else { - fieldAccess = expr.right; - operand = expr.left; - operator = this.negateOperator(operator); - } - - // future()...field should be treated as the "field" directly, so we - // strip 'future().' and synthesize a reference/member-access expr - fieldAccess = this.stripFutureCall(fieldAccess); - - // guard member access of `auth()` with null check - if (this.isAuthOrAuthMemberAccess(operand) && !fieldAccess.$resolvedType?.nullable) { - try { - this.writer.write( - `(${this.plainExprBuilder.transform(operand)} == null) ? ${ - // auth().x != user.x is true when auth().x is null and user is not nullable - // other expressions are evaluated to false when null is involved - operator === '!=' ? TRUE : FALSE - } : ` - ); - } catch (err) { - if (err instanceof TypeScriptExpressionTransformerError) { - throw new PluginError(name, err.message); - } else { - throw err; - } - } - } - - this.block( - () => { - this.writeFieldCondition(fieldAccess, () => { - this.block(() => { - const dataModel = this.isModelTyped(fieldAccess); - if (dataModel && isAuthInvocation(operand)) { - // right now this branch only serves comparison with `auth`, like - // @@allow('all', owner == auth()) - - if (operator !== '==' && operator !== '!=') { - throw new PluginError(name, 'Only == and != operators are allowed'); - } - - if (!isThisExpr(fieldAccess)) { - this.writer.writeLine(operator === '==' ? 'is:' : 'isNot:'); - const fieldIsNullable = !!fieldAccess.$resolvedType?.nullable; - if (fieldIsNullable) { - // if field is nullable, we can generate "null" check condition - this.writer.write(`(user == null) ? null : `); - } - } - - this.block(() => { - if (isThisExpr(fieldAccess) && operator === '!=') { - // negate - this.writer.writeLine('isNot:'); - this.block(() => this.writeIdFieldsCheck(dataModel, operand)); - } else { - this.writeIdFieldsCheck(dataModel, operand); - } - }); - } else { - if (this.equivalentRefs(fieldAccess, operand)) { - // f == f or f != f - // this == this or this != this - this.writer.write(operator === '!=' ? TRUE : FALSE); - } else { - this.writeOperator(operator, fieldAccess, () => { - if (isDataModelFieldReference(operand) && !this.options.isPostGuard) { - // if operand is a field reference and we're not generating for post-update guard, - // we should generate a field reference (comparing fields in the same model) - this.writeFieldReference(operand); - } else { - if (dataModel && this.isModelTyped(operand)) { - // the comparison is between model types, generate id fields comparison block - this.block(() => this.writeIdFieldsCheck(dataModel, operand)); - } else { - // scalar value, just generate the plain expression - this.plain(operand); - } - } - }); - } - } - }, !isThisExpr(fieldAccess)); - }); - }, - // "this" expression is compiled away (to .id access), so we should - // avoid generating a new layer - !isThisExpr(fieldAccess) - ); - } - - private stripFutureCall(fieldAccess: Expression) { - if (!this.isFutureMemberAccess(fieldAccess)) { - return fieldAccess; - } - - const memberAccessStack: MemberAccessExpr[] = []; - let current: Expression = fieldAccess; - while (isMemberAccessExpr(current)) { - memberAccessStack.push(current); - current = current.operand; - } - - const top = memberAccessStack.pop()!; - - // turn the inner-most member access into a reference expr (strip 'future()') - let result: Expression = { - $type: ReferenceExpr, - $container: top.$container, - target: top.member, - $resolvedType: top.$resolvedType, - args: [], - } satisfies ReferenceExpr; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (result as any).$future = true; - - // re-apply member accesses - for (const memberAccess of memberAccessStack.reverse()) { - result = { ...memberAccess, operand: result }; - } - return result; - } - - private isFutureMemberAccess(expr: Expression): expr is MemberAccessExpr { - if (!isMemberAccessExpr(expr)) { - return false; - } - - if (isFutureExpr(expr.operand)) { - return true; - } - - return this.isFutureMemberAccess(expr.operand); - } - - private requireIdFields(dataModel: DataModel) { - const idFields = getIdFields(dataModel); - if (!idFields || idFields.length === 0) { - throw new PluginError(name, `Data model ${dataModel.name} does not have an id field`); - } - return idFields; - } - - private equivalentRefs(expr1: Expression, expr2: Expression) { - if (isThisExpr(expr1) && isThisExpr(expr2)) { - return true; - } - - if ( - isReferenceExpr(expr1) && - isReferenceExpr(expr2) && - expr1.target.ref === expr2.target.ref && - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (expr1 as any).$future === (expr2 as any).$future // either both future or both not - ) { - return true; - } - - return false; - } - - // https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#compare-columns-in-the-same-table - private writeFieldReference(expr: ReferenceExpr) { - if (!expr.target.ref) { - throw new PluginError(name, `Unresolved reference "${expr.target.$refText}"`); - } - const containingModel = expr.target.ref.$container; - this.writer.write(`db.${lowerCaseFirst(containingModel.name)}.fields.${expr.target.ref.name}`); - } - - private isAuthOrAuthMemberAccess(expr: Expression): boolean { - // recursive check for auth().x.y.z - return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && this.isAuthOrAuthMemberAccess(expr.operand)); - } - - private writeOperator(operator: ComparisonOperator, fieldAccess: Expression, writeOperand: () => void) { - if (isDataModel(fieldAccess.$resolvedType?.decl)) { - if (operator === '==') { - this.writer.write('is: '); - } else if (operator === '!=') { - this.writer.write('isNot: '); - } else { - throw new PluginError(name, 'Only == and != operators are allowed for data model comparison'); - } - writeOperand(); - } else { - if (operator === '!=') { - // wrap a 'not' - this.writer.write('not: '); - this.block(() => { - this.writer.write(`${this.mapOperator('==')}: `); - writeOperand(); - }); - } else { - this.writer.write(`${this.mapOperator(operator)}: `); - writeOperand(); - } - } - } - - private writeFieldCondition( - fieldAccess: Expression, - writeCondition: () => void, - filterOp?: FilterOperators, - extraArgs?: Record - ) { - // let selector: string | undefined; - let operand: Expression | undefined; - let fieldWriter: ((conditionWriter: () => void) => void) | undefined; - - if (isThisExpr(fieldAccess)) { - // pass on - writeCondition(); - return; - } else if (isReferenceExpr(fieldAccess)) { - const ref = fieldAccess.target.ref; - invariant(ref); - if (this.isFieldReferenceToDelegateModel(ref)) { - const thisModel = ref.$container as DataModel; - const targetBase = ref.$inheritedFrom; - fieldWriter = (conditionWriter: () => void) => - this.writeBaseHierarchy(thisModel, targetBase, () => { - this.writer.write(`${ref.name}: `); - conditionWriter(); - }); - } else { - fieldWriter = (conditionWriter: () => void) => { - this.writer.write(`${ref.name}: `); - conditionWriter(); - }; - } - } else if (isMemberAccessExpr(fieldAccess)) { - if (!isFutureExpr(fieldAccess.operand)) { - // future().field should be treated as the "field" - operand = fieldAccess.operand; - } - fieldWriter = (conditionWriter: () => void) => { - this.writer.write(`${fieldAccess.member.ref?.name}: `); - conditionWriter(); - }; - } else { - throw new PluginError(name, `Unsupported expression type: ${fieldAccess.$type}`); - } - - if (!fieldWriter) { - throw new PluginError(name, `Failed to write FieldAccess expression`); - } - - const writerFilterOutput = () => { - // this.writer.write(selector + ': '); - fieldWriter!(() => { - if (filterOp) { - this.block(() => { - this.writer.write(`${filterOp}: `); - writeCondition(); - - if (extraArgs) { - for (const [k, v] of Object.entries(extraArgs)) { - this.writer.write(`,\n${k}: `); - this.plain(v); - } - } - }); - } else { - writeCondition(); - } - }); - }; - - if (operand) { - // member access expression - this.writeFieldCondition(operand, () => { - this.block( - writerFilterOutput, - // if operand is "this", it doesn't really generate a new layer of query, - // so we should avoid generating a new block - !isThisExpr(operand) - ); - }); - } else { - writerFilterOutput(); - } - } - - private block(write: () => void, condition = true) { - if (condition) { - this.writer.inlineBlock(write); - } else { - write(); - } - } - - private isModelTyped(expr: Expression) { - return isDataModel(expr.$resolvedType?.decl) ? (expr.$resolvedType?.decl as DataModel) : undefined; - } - - private mapOperator(operator: '==' | '!=' | '>' | '>=' | '<' | '<=') { - switch (operator) { - case '==': - return 'equals'; - case '!=': - throw new Error('Operation != should have been compiled away'); - case '>': - return 'gt'; - case '>=': - return 'gte'; - case '<': - return 'lt'; - case '<=': - return 'lte'; - } - } - - private negateOperator(operator: '==' | '!=' | '>' | '>=' | '<' | '<=') { - switch (operator) { - case '>': - return '<='; - case '<': - return '>='; - case '>=': - return '<'; - case '<=': - return '>'; - default: - return operator; - } - } - - private writeLogical(expr: BinaryExpr, operator: '&&' | '||') { - // TODO: do we need short-circuit for logical operators? - - if (operator === '&&') { - // // && short-circuit: left && right -> left ? right : FALSE - // if (!this.hasFieldAccess(expr.left)) { - // this.plain(expr.left); - // this.writer.write(' ? '); - // this.write(expr.right); - // this.writer.write(' : '); - // this.block(() => this.guard(() => this.writer.write('false'))); - // } else { - this.block(() => { - this.writer.write('AND:'); - this.writeExprList([expr.left, expr.right]); - }); - // } - } else { - // // || short-circuit: left || right -> left ? TRUE : right - // if (!this.hasFieldAccess(expr.left)) { - // this.plain(expr.left); - // this.writer.write(' ? '); - // this.block(() => this.guard(() => this.writer.write('true'))); - // this.writer.write(' : '); - // this.write(expr.right); - // } else { - this.block(() => { - this.writer.write('OR:'); - this.writeExprList([expr.left, expr.right]); - }); - // } - } - } - - private writeUnary(expr: UnaryExpr) { - if (expr.operator !== '!') { - throw new PluginError(name, `Unary operator "${expr.operator}" is not supported`); - } - - this.block(() => { - this.writer.write('NOT: '); - this.write(expr.operand); - }); - } - - private writeLiteral(expr: LiteralExpr) { - if (expr.value === true) { - this.writer.write(TRUE); - } else if (expr.value === false) { - this.writer.write(FALSE); - } else { - this.guard(() => { - this.plain(expr); - }); - } - } - - private writeInvocation(expr: InvocationExpr) { - const funcDecl = expr.function.ref; - if (!funcDecl) { - throw new PluginError(name, `Failed to resolve function declaration`); - } - - const functionAllowedContext = getFunctionExpressionContext(funcDecl); - if ( - functionAllowedContext.includes(ExpressionContext.AccessPolicy) || - functionAllowedContext.includes(ExpressionContext.ValidationRule) - ) { - if (isCheckInvocation(expr)) { - this.writeRelationCheck(expr); - return; - } - - if (!expr.args.some((arg) => this.isFieldAccess(arg.value))) { - // filter functions without referencing fields - this.guard(() => this.plain(expr)); - return; - } - - let valueArg = expr.args[1]?.value; - - // isEmpty function is zero arity, it's mapped to a boolean literal - if (isFromStdlib(funcDecl) && funcDecl.name === 'isEmpty') { - valueArg = { $type: BooleanLiteral, value: true } as LiteralExpr; - } - - // contains function has a 3rd argument that indicates whether the comparison should be case-insensitive - let extraArgs: Record | undefined = undefined; - if (isFromStdlib(funcDecl) && funcDecl.name === 'contains') { - if (getLiteral(expr.args[2]?.value) === true) { - extraArgs = { mode: { $type: StringLiteral, value: 'insensitive' } as LiteralExpr }; - } - } - - this.block(() => { - this.writeFieldCondition( - expr.args[0].value, - () => { - this.plain(valueArg); - }, - funcDecl.name as FilterOperators, - extraArgs - ); - }); - } else { - throw new PluginError(name, `Unsupported function ${funcDecl.name}`); - } - } - - private writeRelationCheck(expr: InvocationExpr) { - if (!isDataModelFieldReference(expr.args[0].value)) { - throw new PluginError(name, `First argument of check() must be a field`); - } - if (!isDataModel(expr.args[0].value.$resolvedType?.decl)) { - throw new PluginError(name, `First argument of check() must be a relation field`); - } - - const fieldRef = expr.args[0].value; - const targetModel = fieldRef.$resolvedType?.decl as DataModel; - - let operation: string; - if (expr.args[1]) { - const literal = getLiteral(expr.args[1].value); - if (!literal) { - throw new TypeScriptExpressionTransformerError(`Second argument of check() must be a string literal`); - } - if (!['read', 'create', 'update', 'delete'].includes(literal)) { - throw new TypeScriptExpressionTransformerError(`Invalid check() operation "${literal}"`); - } - operation = literal; - } else { - if (!this.options.operationContext) { - throw new TypeScriptExpressionTransformerError('Unable to determine CRUD operation from context'); - } - operation = this.options.operationContext; - } - - this.block(() => - this.writeFieldCondition(fieldRef, () => { - if (operation === 'postUpdate') { - // 'postUpdate' policies are not delegated to relations, just use constant `false` here - // e.g.: - // @@allow('all', check(author)) should not delegate "postUpdate" to author - this.writer.write(FALSE); - } else { - const targetGuardFunc = getQueryGuardFunctionName(targetModel, undefined, false, operation); - this.writer.write(`${targetGuardFunc}(context, db)`); - } - }) - ); - } -} diff --git a/packages/schema/src/plugins/enhancer/policy/index.ts b/packages/schema/src/plugins/enhancer/policy/index.ts deleted file mode 100644 index 918bfba8c..000000000 --- a/packages/schema/src/plugins/enhancer/policy/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type PluginOptions } from '@zenstackhq/sdk'; -import type { Model } from '@zenstackhq/sdk/ast'; -import type { Project } from 'ts-morph'; -import { PolicyGenerator } from './policy-guard-generator'; - -export async function generate(model: Model, options: PluginOptions, project: Project, outDir: string) { - return new PolicyGenerator(options).generate(project, model, outDir); -} diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts deleted file mode 100644 index f257f0830..000000000 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ /dev/null @@ -1,748 +0,0 @@ -import { - DataModel, - DataModelField, - Expression, - InvocationExpr, - Model, - ReferenceExpr, - isDataModel, - isDataModelField, - isEnum, - isMemberAccessExpr, - isReferenceExpr, - isThisExpr, - isTypeDef, -} from '@zenstackhq/language/ast'; -import { PolicyCrudKind, type PolicyOperationKind } from '@zenstackhq/runtime'; -import { - type CodeWriter, - ExpressionContext, - FastWriter, - PluginOptions, - PolicyAnalysisResult, - RUNTIME_PACKAGE, - TypeScriptExpressionTransformer, - analyzePolicies, - getDataModels, - hasAttribute, - hasValidationAttributes, - isAuthInvocation, - isForeignKeyField, - saveSourceFile, -} from '@zenstackhq/sdk'; -import { getPrismaClientImportSpec } from '@zenstackhq/sdk/prisma'; -import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { streamAst } from 'langium'; -import path from 'path'; -import { FunctionDeclarationStructure, OptionalKind, Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; -import { isCheckInvocation } from '../../../utils/ast-utils'; -import { ConstraintTransformer } from './constraint-transformer'; -import { - generateConstantQueryGuardFunction, - generateEntityCheckerFunction, - generateNormalizedAuthRef, - generateQueryGuardFunction, - generateSelectForRules, - getPolicyExpressions, - isEnumReferenced, -} from './utils'; - -/** - * Generates source file that contains Prisma query guard objects used for injecting database queries - */ -export class PolicyGenerator { - private extraFunctions: OptionalKind[] = []; - - constructor(private options: PluginOptions) {} - - generate(project: Project, model: Model, output: string) { - const sf = project.createSourceFile(path.join(output, 'policy.ts'), undefined, { overwrite: true }); - - this.writeImports(model, output, sf); - - const models = getDataModels(model); - - const writer = new FastWriter(); - writer.block(() => { - this.writePolicy(writer, models); - this.writeValidationMeta(writer, models); - this.writeAuthSelector(models, writer); - }); - - sf.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: 'policy', - type: 'PolicyDef', - initializer: writer.result, - }, - ], - }); - - if (this.extraFunctions.length > 0) { - sf.addFunctions(this.extraFunctions); - } - - sf.addStatements('export default policy'); - - // save ts files if requested explicitly or the user provided - const preserveTsFiles = this.options.preserveTsFiles === true || !!this.options.output; - if (preserveTsFiles) { - saveSourceFile(sf); - } - } - - private writeImports(model: Model, output: string, sf: SourceFile) { - sf.addImportDeclaration({ - namedImports: [ - { name: 'type QueryContext' }, - { name: 'type CrudContract' }, - { name: 'type PermissionCheckerContext' }, - ], - moduleSpecifier: `${RUNTIME_PACKAGE}`, - }); - - sf.addImportDeclaration({ - namedImports: [{ name: 'allFieldsEqual' }], - moduleSpecifier: `${RUNTIME_PACKAGE}/validation`, - }); - - sf.addImportDeclaration({ - namedImports: [{ name: 'type PolicyDef' }, { name: 'type PermissionCheckerConstraint' }], - moduleSpecifier: `${RUNTIME_PACKAGE}/enhancements/node`, - }); - - // import enums - const prismaImport = getPrismaClientImportSpec(output, this.options); - for (const e of model.declarations.filter((d) => isEnum(d) && isEnumReferenced(model, d))) { - sf.addImportDeclaration({ - namedImports: [{ name: e.name }], - moduleSpecifier: prismaImport, - }); - } - } - - private writePolicy(writer: CodeWriter, models: DataModel[]) { - writer.write('policy:'); - writer.inlineBlock(() => { - for (const model of models) { - writer.write(`${lowerCaseFirst(model.name)}:`); - - writer.block(() => { - // model-level guards - this.writeModelLevelDefs(model, writer); - - // field-level guards - this.writeFieldLevelDefs(model, writer); - }); - - writer.writeLine(','); - } - }); - writer.writeLine(','); - } - - // #region Model-level definitions - - // writes model-level policy def for each operation kind for a model - // `[modelName]: { [operationKind]: [funcName] },` - private writeModelLevelDefs(model: DataModel, writer: CodeWriter) { - const policies = analyzePolicies(model); - writer.write('modelLevel:'); - writer.inlineBlock(() => { - this.writeModelReadDef(model, policies, writer); - this.writeModelCreateDef(model, policies, writer); - this.writeModelUpdateDef(model, policies, writer); - this.writeModelPostUpdateDef(model, policies, writer); - this.writeModelDeleteDef(model, policies, writer); - }); - writer.writeLine(','); - } - - // writes `read: ...` for a given model - private writeModelReadDef(model: DataModel, policies: PolicyAnalysisResult, writer: CodeWriter) { - writer.write(`read:`); - writer.inlineBlock(() => { - this.writeCommonModelDef(model, 'read', policies, writer); - }); - writer.writeLine(','); - } - - // writes `create: ...` for a given model - private writeModelCreateDef(model: DataModel, policies: PolicyAnalysisResult, writer: CodeWriter) { - writer.write(`create:`); - writer.inlineBlock(() => { - this.writeCommonModelDef(model, 'create', policies, writer); - - // create policy has an additional input checker for validating the payload - this.writeCreateInputChecker(model, writer); - }); - writer.writeLine(','); - } - - // writes `inputChecker: [funcName]` for a given model - private writeCreateInputChecker(model: DataModel, writer: CodeWriter) { - if (this.canCheckCreateBasedOnInput(model)) { - const inputCheckFuncName = this.generateCreateInputCheckerFunction(model); - writer.write(`inputChecker: ${inputCheckFuncName},`); - } - } - - private canCheckCreateBasedOnInput(model: DataModel) { - const allows = getPolicyExpressions(model, 'allow', 'create', false, 'all'); - const denies = getPolicyExpressions(model, 'deny', 'create', false, 'all'); - - return [...allows, ...denies].every((rule) => { - return streamAst(rule).every((expr) => { - if (isThisExpr(expr)) { - return false; - } - if (isReferenceExpr(expr)) { - if (isDataModel(expr.$resolvedType?.decl)) { - // if policy rules uses relation fields, - // we can't check based on create input - return false; - } - - if ( - isDataModelField(expr.target.ref) && - expr.target.ref.$container === model && - hasAttribute(expr.target.ref, '@default') - ) { - // reference to field of current model - // if it has default value, we can't check - // based on create input - return false; - } - - if (isDataModelField(expr.target.ref) && isForeignKeyField(expr.target.ref)) { - // reference to foreign key field - // we can't check based on create input - return false; - } - } - - return true; - }); - }); - } - - // generates a function for checking "create" input - private generateCreateInputCheckerFunction(model: DataModel) { - const statements: string[] = []; - const allows = getPolicyExpressions(model, 'allow', 'create'); - const denies = getPolicyExpressions(model, 'deny', 'create'); - - generateNormalizedAuthRef(model, allows, denies, statements); - - // write allow and deny rules - const writer = new FastWriter(); - if (allows.length === 0) { - writer.write('return false;'); - } else { - const transformer = new TypeScriptExpressionTransformer({ - context: ExpressionContext.AccessPolicy, - fieldReferenceContext: 'input', - operationContext: 'create', - }); - - let expr = - denies.length > 0 - ? '!(' + - denies - .map((deny) => { - return transformer.transform(deny, false); - }) - .join(' || ') + - ')' - : undefined; - - const allowStmt = allows - .map((allow) => { - return transformer.transform(allow, false); - }) - .join(' || '); - - expr = expr ? `${expr} && (${allowStmt})` : allowStmt; - writer.write('return ' + expr); - } - statements.push(writer.result); - - const funcName = model.name + '_create_input'; - this.extraFunctions.push({ - name: funcName, - returnType: 'boolean', - parameters: [ - { - name: 'input', - type: 'any', - }, - { - name: 'context', - type: 'QueryContext', - }, - ], - statements, - }); - - return funcName; - } - - // writes `update: ...` for a given model - private writeModelUpdateDef(model: DataModel, policies: PolicyAnalysisResult, writer: CodeWriter) { - writer.write(`update:`); - writer.inlineBlock(() => { - this.writeCommonModelDef(model, 'update', policies, writer); - }); - writer.writeLine(','); - } - - // writes `postUpdate: ...` for a given model - private writeModelPostUpdateDef(model: DataModel, policies: PolicyAnalysisResult, writer: CodeWriter) { - writer.write(`postUpdate:`); - writer.inlineBlock(() => { - this.writeCommonModelDef(model, 'postUpdate', policies, writer); - - // post-update policy has an additional selector for reading the pre-update entity data - this.writePostUpdatePreValueSelector(model, writer); - }); - writer.writeLine(','); - } - - private writePostUpdatePreValueSelector(model: DataModel, writer: CodeWriter) { - const allows = getPolicyExpressions(model, 'allow', 'postUpdate'); - const denies = getPolicyExpressions(model, 'deny', 'postUpdate'); - const preValueSelect = generateSelectForRules([...allows, ...denies], 'postUpdate'); - if (preValueSelect) { - writer.writeLine(`preUpdateSelector: ${JSON.stringify(preValueSelect)},`); - } - } - - // writes `delete: ...` for a given model - private writeModelDeleteDef(model: DataModel, policies: PolicyAnalysisResult, writer: CodeWriter) { - writer.write(`delete:`); - writer.inlineBlock(() => { - this.writeCommonModelDef(model, 'delete', policies, writer); - }); - } - - // writes `[kind]: ...` for a given model - private writeCommonModelDef( - model: DataModel, - kind: PolicyOperationKind, - policies: PolicyAnalysisResult, - writer: CodeWriter - ) { - const allows = getPolicyExpressions(model, 'allow', kind); - const denies = getPolicyExpressions(model, 'deny', kind); - - // policy guard - this.writePolicyGuard(model, kind, policies, allows, denies, writer); - - // permission checker - if (kind !== 'postUpdate') { - this.writePermissionChecker(model, kind, policies, allows, denies, writer); - } - - // write cross-model comparison rules as entity checker functions - // because they cannot be checked inside Prisma - const { functionName, selector } = this.writeEntityChecker(model, kind, false); - - if (this.shouldUseEntityChecker(model, kind, true, false)) { - writer.write(`entityChecker: { func: ${functionName}, selector: ${JSON.stringify(selector)} },`); - } - } - - private shouldUseEntityChecker( - target: DataModel | DataModelField, - kind: PolicyOperationKind, - onlyCrossModelComparison: boolean, - forOverride: boolean - ): boolean { - const allows = getPolicyExpressions( - target, - 'allow', - kind, - forOverride, - onlyCrossModelComparison ? 'onlyCrossModelComparison' : 'all' - ); - const denies = getPolicyExpressions( - target, - 'deny', - kind, - forOverride, - onlyCrossModelComparison ? 'onlyCrossModelComparison' : 'all' - ); - - if (allows.length > 0 || denies.length > 0) { - return true; - } - - const allRules = [ - ...getPolicyExpressions(target, 'allow', kind, forOverride, 'all'), - ...getPolicyExpressions(target, 'deny', kind, forOverride, 'all'), - ]; - - return allRules.some((rule) => { - return streamAst(rule).some((node) => { - if (isCheckInvocation(node)) { - const expr = node as InvocationExpr; - const fieldRef = expr.args[0].value as ReferenceExpr; - const targetModel = fieldRef.$resolvedType?.decl as DataModel; - return this.shouldUseEntityChecker(targetModel, kind, onlyCrossModelComparison, forOverride); - } - return false; - }); - }); - } - - private writeEntityChecker(target: DataModel | DataModelField, kind: PolicyOperationKind, forOverride: boolean) { - const allows = getPolicyExpressions(target, 'allow', kind, forOverride, 'all'); - const denies = getPolicyExpressions(target, 'deny', kind, forOverride, 'all'); - - const model = isDataModel(target) ? target : (target.$container as DataModel); - const func = generateEntityCheckerFunction( - model, - kind, - allows, - denies, - isDataModelField(target) ? target : undefined, - forOverride - ); - this.extraFunctions.push(func); - const selector = generateSelectForRules([...allows, ...denies], kind, false, kind !== 'postUpdate') ?? {}; - - return { functionName: func.name, selector }; - } - - // writes `guard: ...` for a given policy operation kind - private writePolicyGuard( - model: DataModel, - kind: PolicyOperationKind, - policies: ReturnType, - allows: Expression[], - denies: Expression[], - writer: CodeWriter - ) { - // first handle several cases where a constant function can be used - - if (kind === 'update' && allows.length === 0) { - // no allow rule for 'update', policy is constant based on if there's - // post-update counterpart - let func: OptionalKind; - if (getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) { - func = generateConstantQueryGuardFunction(model, kind, false); - } else { - func = generateConstantQueryGuardFunction(model, kind, true); - } - this.extraFunctions.push(func); - writer.write(`guard: ${func.name},`); - return; - } - - if (kind === 'postUpdate' && allows.length === 0 && denies.length === 0) { - // no 'postUpdate' rule, always allow - const func = generateConstantQueryGuardFunction(model, kind, true); - this.extraFunctions.push(func); - writer.write(`guard: ${func.name},`); - return; - } - - if (kind in policies && typeof policies[kind as keyof typeof policies] === 'boolean') { - // constant policy - const func = generateConstantQueryGuardFunction( - model, - kind, - policies[kind as keyof typeof policies] as boolean - ); - this.extraFunctions.push(func); - writer.write(`guard: ${func.name},`); - return; - } - - // generate a policy function that evaluates a partial prisma query - const guardFunc = generateQueryGuardFunction(model, kind, allows, denies); - this.extraFunctions.push(guardFunc); - writer.write(`guard: ${guardFunc.name},`); - } - - // writes `permissionChecker: ...` for a given policy operation kind - private writePermissionChecker( - model: DataModel, - kind: PolicyCrudKind, - policies: PolicyAnalysisResult, - allows: Expression[], - denies: Expression[], - writer: CodeWriter - ) { - if (this.options.generatePermissionChecker !== true) { - return; - } - - if (policies[kind] === true || policies[kind] === false) { - // constant policy - writer.write(`permissionChecker: ${policies[kind]},`); - return; - } - - if (kind === 'update' && allows.length === 0) { - // no allow rule for 'update', policy is constant based on if there's - // post-update counterpart - if (getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) { - writer.write(`permissionChecker: false,`); - } else { - writer.write(`permissionChecker: true,`); - } - return; - } - - const guardFuncName = this.generatePermissionCheckerFunction(model, kind, allows, denies); - writer.write(`permissionChecker: ${guardFuncName},`); - } - - private generatePermissionCheckerFunction( - model: DataModel, - kind: string, - allows: Expression[], - denies: Expression[] - ) { - const statements: string[] = []; - - generateNormalizedAuthRef(model, allows, denies, statements); - - const transformed = new ConstraintTransformer({ - authAccessor: 'user', - }).transformRules(allows, denies); - - statements.push(`return ${transformed};`); - - const funcName = `${model.name}$checker$${kind}`; - this.extraFunctions.push({ - name: funcName, - returnType: 'PermissionCheckerConstraint', - parameters: [ - { - name: 'context', - type: 'PermissionCheckerContext', - }, - ], - statements, - }); - - return funcName; - } - - // #endregion - - // #region Field-level definitions - - private writeFieldLevelDefs(model: DataModel, writer: CodeWriter) { - writer.write('fieldLevel:'); - writer.inlineBlock(() => { - this.writeFieldReadDef(model, writer); - this.writeFieldUpdateDef(model, writer); - }); - writer.writeLine(','); - } - - private writeFieldReadDef(model: DataModel, writer: CodeWriter) { - writer.writeLine('read:'); - writer.block(() => { - for (const field of model.fields) { - const allows = getPolicyExpressions(field, 'allow', 'read'); - const denies = getPolicyExpressions(field, 'deny', 'read'); - const overrideAllows = getPolicyExpressions(field, 'allow', 'read', true); - - if (allows.length === 0 && denies.length === 0 && overrideAllows.length === 0) { - continue; - } - - writer.write(`${field.name}:`); - - writer.block(() => { - // guard - const guardFunc = generateQueryGuardFunction(model, 'read', allows, denies, field); - this.extraFunctions.push(guardFunc); - writer.write(`guard: ${guardFunc.name},`); - - // checker function - // write all field-level rules as entity checker function - const { functionName, selector } = this.writeEntityChecker(field, 'read', false); - - if (this.shouldUseEntityChecker(field, 'read', false, false)) { - writer.write( - `entityChecker: { func: ${functionName}, selector: ${JSON.stringify(selector)} },` - ); - } - - if (overrideAllows.length > 0) { - // override guard function - const denies = getPolicyExpressions(field, 'deny', 'read'); - const overrideGuardFunc = generateQueryGuardFunction( - model, - 'read', - overrideAllows, - denies, - field, - true - ); - this.extraFunctions.push(overrideGuardFunc); - writer.write(`overrideGuard: ${overrideGuardFunc.name},`); - - // additional entity checker for override - const { functionName, selector } = this.writeEntityChecker(field, 'read', true); - if (this.shouldUseEntityChecker(field, 'read', false, true)) { - writer.write( - `overrideEntityChecker: { func: ${functionName}, selector: ${JSON.stringify( - selector - )} },` - ); - } - } - }); - writer.writeLine(','); - } - }); - writer.writeLine(','); - } - - private writeFieldUpdateDef(model: DataModel, writer: CodeWriter) { - writer.writeLine('update:'); - writer.block(() => { - for (const field of model.fields) { - const allows = getPolicyExpressions(field, 'allow', 'update'); - const denies = getPolicyExpressions(field, 'deny', 'update'); - const overrideAllows = getPolicyExpressions(field, 'allow', 'update', true); - - if (allows.length === 0 && denies.length === 0 && overrideAllows.length === 0) { - continue; - } - - writer.write(`${field.name}:`); - - writer.block(() => { - // guard - const guardFunc = generateQueryGuardFunction(model, 'update', allows, denies, field); - this.extraFunctions.push(guardFunc); - writer.write(`guard: ${guardFunc.name},`); - - // write cross-model comparison rules as entity checker functions - // because they cannot be checked inside Prisma - const { functionName, selector } = this.writeEntityChecker(field, 'update', false); - if (this.shouldUseEntityChecker(field, 'update', true, false)) { - writer.write( - `entityChecker: { func: ${functionName}, selector: ${JSON.stringify(selector)} },` - ); - } - - if (overrideAllows.length > 0) { - // override guard - const overrideGuardFunc = generateQueryGuardFunction( - model, - 'update', - overrideAllows, - denies, - field, - true - ); - this.extraFunctions.push(overrideGuardFunc); - writer.write(`overrideGuard: ${overrideGuardFunc.name},`); - - // write cross-model comparison override rules as entity checker functions - // because they cannot be checked inside Prisma - const { functionName, selector } = this.writeEntityChecker(field, 'update', true); - if (this.shouldUseEntityChecker(field, 'update', true, true)) { - writer.write( - `overrideEntityChecker: { func: ${functionName}, selector: ${JSON.stringify( - selector - )} },` - ); - } - } - }); - writer.writeLine(','); - } - }); - writer.writeLine(','); - } - - // #endregion - - //#region Auth selector - - private writeAuthSelector(models: DataModel[], writer: CodeWriter) { - const authSelector = this.generateAuthSelector(models); - if (authSelector) { - writer.write(`authSelector: ${JSON.stringify(authSelector)},`); - } - } - - // Generates a { select: ... } object to select `auth()` fields used in policy rules - private generateAuthSelector(models: DataModel[]) { - const authRules: Expression[] = []; - - models.forEach((model) => { - // model-level rules - const modelPolicyAttrs = model.attributes.filter((attr) => - ['@@allow', '@@deny'].includes(attr.decl.$refText) - ); - - // field-level rules - const fieldPolicyAttrs = model.fields - .flatMap((f) => f.attributes) - .filter((attr) => ['@allow', '@deny'].includes(attr.decl.$refText)); - - // all rule expression - const allExpressions = [...modelPolicyAttrs, ...fieldPolicyAttrs] - .filter((attr) => attr.args.length > 1) - .map((attr) => attr.args[1].value); - - // collect `auth()` member access - allExpressions.forEach((rule) => { - streamAst(rule).forEach((node) => { - if (isMemberAccessExpr(node) && isAuthInvocation(node.operand)) { - authRules.push(node); - } - }); - }); - }); - - if (authRules.length > 0) { - return generateSelectForRules(authRules, undefined, true); - } else { - return undefined; - } - } - - // #endregion - - // #region Validation meta - - private writeValidationMeta(writer: CodeWriter, models: DataModel[]) { - writer.write('validation:'); - writer.inlineBlock(() => { - for (const model of models) { - writer.write(`${lowerCaseFirst(model.name)}:`); - writer.inlineBlock(() => { - writer.write( - `hasValidation: ${ - // explicit validation rules - hasValidationAttributes(model) || - // type-def fields require schema validation - this.hasTypeDefFields(model) - }` - ); - }); - writer.writeLine(','); - } - }); - writer.writeLine(','); - } - - private hasTypeDefFields(model: DataModel): boolean { - return model.fields.some((f) => isTypeDef(f.type.reference?.ref)); - } - - // #endregion -} diff --git a/packages/schema/src/plugins/enhancer/policy/utils.ts b/packages/schema/src/plugins/enhancer/policy/utils.ts deleted file mode 100644 index ae9a7846f..000000000 --- a/packages/schema/src/plugins/enhancer/policy/utils.ts +++ /dev/null @@ -1,592 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { PolicyKind, PolicyOperationKind } from '@zenstackhq/runtime'; -import { - ExpressionContext, - FastWriter, - PluginError, - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, - getAttributeArg, - getAuthDecl, - getDataModelAndTypeDefs, - getDataModels, - getEntityCheckerFunctionName, - getIdFields, - getLiteral, - getQueryGuardFunctionName, - isAuthInvocation, - isDataModelFieldReference, - isEnumFieldReference, - isFromStdlib, - isFutureExpr, - resolved, -} from '@zenstackhq/sdk'; -import { - Enum, - InvocationExpr, - Model, - ReferenceExpr, - isBinaryExpr, - isDataModel, - isDataModelField, - isExpression, - isInvocationExpr, - isMemberAccessExpr, - isReferenceExpr, - isThisExpr, - type DataModel, - type DataModelField, - type Expression, -} from '@zenstackhq/sdk/ast'; -import deepmerge from 'deepmerge'; -import { getContainerOfType, streamAllContents, streamAst, streamContents } from 'langium'; -import { FunctionDeclarationStructure, OptionalKind } from 'ts-morph'; -import { name } from '..'; -import { isCheckInvocation, isCollectionPredicate, isFutureInvocation } from '../../../utils/ast-utils'; -import { ExpressionWriter, FALSE, TRUE } from './expression-writer'; - -/** - * Get policy expressions for the given model or field and operation kind - */ -export function getPolicyExpressions( - target: DataModel | DataModelField, - kind: PolicyKind, - operation: PolicyOperationKind, - forOverride = false, - filter: 'all' | 'withoutCrossModelComparison' | 'onlyCrossModelComparison' = 'all' -) { - const attributes = target.attributes; - const attrName = isDataModel(target) ? `@@${kind}` : `@${kind}`; - const attrs = attributes.filter((attr) => { - if (attr.decl.ref?.name !== attrName) { - return false; - } - - const overrideArg = getAttributeArg(attr, 'override'); - const isOverride = !!overrideArg && getLiteral(overrideArg) === true; - - return (forOverride && isOverride) || (!forOverride && !isOverride); - }); - - const checkOperation = operation === 'postUpdate' ? 'update' : operation; - - let result = attrs - .filter((attr) => { - const opsValue = getLiteral(attr.args[0].value); - if (!opsValue) { - return false; - } - const ops = opsValue.split(',').map((s) => s.trim()); - return ops.includes(checkOperation) || ops.includes('all'); - }) - .map((attr) => attr.args[1].value); - - if (filter === 'onlyCrossModelComparison') { - result = result.filter((expr) => hasCrossModelComparison(expr)); - } else if (filter === 'withoutCrossModelComparison') { - result = result.filter((expr) => !hasCrossModelComparison(expr)); - } - - if (operation === 'update') { - result = processUpdatePolicies(result, false); - } else if (operation === 'postUpdate') { - result = processUpdatePolicies(result, true); - } - - return result; -} - -function hasFutureReference(expr: Expression) { - for (const node of streamAst(expr)) { - if (isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref)) { - return true; - } - } - return false; -} - -function processUpdatePolicies(expressions: Expression[], postUpdate: boolean) { - const hasFutureRef = expressions.some(hasFutureReference); - if (postUpdate) { - // when compiling post-update rules, if any rule contains `future()` reference, - // we include all as post-update rules - return hasFutureRef ? expressions : []; - } else { - // when compiling pre-update rules, if any rule contains `future()` reference, - // we completely skip pre-update check and defer them to post-update - return hasFutureRef ? [] : expressions; - } -} - -/** - * Generates a "select" object that contains (recursively) fields referenced by the - * given policy rules - */ -export function generateSelectForRules( - rules: Expression[], - forOperation: PolicyOperationKind | undefined, - forAuthContext = false, - ignoreFutureReference = true -) { - let result: any = {}; - const addPath = (path: string[]) => { - const thisIndex = path.lastIndexOf('$this'); - if (thisIndex >= 0) { - // drop everything before $this - path = path.slice(thisIndex + 1); - } - let curr = result; - path.forEach((seg, i) => { - if (i === path.length - 1) { - curr[seg] = true; - } else { - if (!curr[seg]) { - curr[seg] = { select: {} }; - } - curr = curr[seg].select; - } - }); - }; - - // visit a reference or member access expression to build a - // selection path - const visit = (node: Expression): string[] | undefined => { - if (isThisExpr(node)) { - return ['$this']; - } - - if (isFutureExpr(node)) { - return []; - } - - if (isReferenceExpr(node)) { - const target = resolved(node.target); - if (isDataModelField(target)) { - // a field selection, it's a terminal - return [target.name]; - } - } - - if (isMemberAccessExpr(node)) { - if (forAuthContext && isAuthInvocation(node.operand)) { - return [node.member.$refText]; - } - - if (isFutureExpr(node.operand) && ignoreFutureReference) { - // future().field is not subject to pre-update select - return undefined; - } - - // build a selection path inside-out for chained member access - const inner = visit(node.operand); - if (inner) { - return [...inner, node.member.$refText]; - } - } - - return undefined; - }; - - // collect selection paths from the given expression - const collectReferencePaths = (expr: Expression): string[][] => { - if (isThisExpr(expr) && !isMemberAccessExpr(expr.$container)) { - // a standalone `this` expression, include all id fields - const model = expr.$resolvedType?.decl as DataModel; - const idFields = getIdFields(model); - return idFields.map((field) => [field.name]); - } - - if (isMemberAccessExpr(expr) || isReferenceExpr(expr)) { - const path = visit(expr); - if (path) { - if (isDataModel(expr.$resolvedType?.decl)) { - // member selection ended at a data model field, include its id fields - const idFields = getIdFields(expr.$resolvedType?.decl as DataModel); - return idFields.map((field) => [...path, field.name]); - } else { - return [path]; - } - } else { - return []; - } - } else if (isCollectionPredicate(expr)) { - const path = visit(expr.left); - // recurse into RHS - const rhs = collectReferencePaths(expr.right); - if (path) { - // combine path of LHS and RHS - return rhs.map((r) => [...path, ...r]); - } else { - // LHS is not rooted from the current model, - // only keep RHS items that contains '$this' - return rhs.filter((r) => r.includes('$this')); - } - } else if (isInvocationExpr(expr)) { - // recurse into function arguments - return expr.args.flatMap((arg) => collectReferencePaths(arg.value)); - } else { - // recurse - const children = streamContents(expr) - .filter((child): child is Expression => isExpression(child)) - .toArray(); - return children.flatMap((child) => collectReferencePaths(child)); - } - }; - - for (const rule of rules) { - const paths = collectReferencePaths(rule); - paths.forEach((p) => addPath(p)); - - // merge selectors from models referenced by `check()` calls - streamAst(rule).forEach((node) => { - if (isCheckInvocation(node)) { - const expr = node as InvocationExpr; - const fieldRef = expr.args[0].value as ReferenceExpr; - const targetModel = fieldRef.$resolvedType?.decl as DataModel; - const targetOperation = getLiteral(expr.args[1]?.value) ?? forOperation; - const targetSelector = generateSelectForRules( - [ - ...getPolicyExpressions(targetModel, 'allow', targetOperation as PolicyOperationKind), - ...getPolicyExpressions(targetModel, 'deny', targetOperation as PolicyOperationKind), - ], - targetOperation as PolicyOperationKind, - forAuthContext, - ignoreFutureReference - ); - if (targetSelector) { - result = deepmerge(result, { [fieldRef.target.$refText]: { select: targetSelector } }); - } - } - }); - } - - return Object.keys(result).length === 0 ? undefined : result; -} - -/** - * Generates a constant query guard function - */ -export function generateConstantQueryGuardFunction(model: DataModel, kind: PolicyOperationKind, value: boolean) { - return { - name: getQueryGuardFunctionName(model, undefined, false, kind), - returnType: 'any', - parameters: [ - { - name: 'context', - type: 'QueryContext', - }, - { - // for generating field references used by field comparison in the same model - name: 'db', - type: 'CrudContract', - }, - ], - statements: [`return ${value ? TRUE : FALSE};`], - } as OptionalKind; -} - -/** - * Generates a query guard function that returns a partial Prisma query for the given model or field - */ -export function generateQueryGuardFunction( - model: DataModel, - kind: PolicyOperationKind, - allows: Expression[], - denies: Expression[], - forField?: DataModelField, - fieldOverride = false -) { - const statements: string[] = []; - - const allowRules = allows.filter((rule) => !hasCrossModelComparison(rule)); - const denyRules = denies.filter((rule) => !hasCrossModelComparison(rule)); - - generateNormalizedAuthRef(model, allowRules, denyRules, statements); - - const hasFieldAccess = [...denyRules, ...allowRules].some((rule) => - streamAst(rule).some( - (child) => - // this.??? - isThisExpr(child) || - // future().??? - isFutureExpr(child) || - // field reference - (isReferenceExpr(child) && isDataModelField(child.target.ref)) - ) - ); - - if (!hasFieldAccess) { - // none of the rules reference model fields, we can compile down to a plain boolean - // function in this case (so we can skip doing SQL queries when validating) - const writer = new FastWriter(); - const transformer = new TypeScriptExpressionTransformer({ - context: ExpressionContext.AccessPolicy, - isPostGuard: kind === 'postUpdate', - operationContext: kind, - }); - try { - denyRules.forEach((rule) => { - writer.write(`if (${transformer.transform(rule, false)}) { return ${FALSE}; }`); - }); - allowRules.forEach((rule) => { - writer.write(`if (${transformer.transform(rule, false)}) { return ${TRUE}; }`); - }); - } catch (err) { - if (err instanceof TypeScriptExpressionTransformerError) { - throw new PluginError(name, err.message); - } else { - throw err; - } - } - - if (forField) { - if (allows.length === 0) { - // if there's no allow rule, for field-level rules, by default we allow - writer.write(`return ${TRUE};`); - } else { - if (allowRules.length < allows.length) { - writer.write(`return ${TRUE};`); - } else { - // if there's any allow rule, we deny unless any allow rule evaluates to true - writer.write(`return ${FALSE};`); - } - } - } else { - if (allowRules.length < allows.length) { - // some rules are filtered out here and will be generated as additional - // checker functions, so we allow here to avoid a premature denial - writer.write(`return ${TRUE};`); - } else { - // for model-level rules, the default is always deny unless for 'postUpdate' - writer.write(`return ${kind === 'postUpdate' ? TRUE : FALSE};`); - } - } - - statements.push(writer.result); - } else { - const writer = new FastWriter(); - writer.write('return '); - const exprWriter = new ExpressionWriter(writer, { - isPostGuard: kind === 'postUpdate', - operationContext: kind, - }); - const writeDenies = () => { - writer.conditionalWrite(denyRules.length > 1, '{ AND: ['); - denyRules.forEach((expr, i) => { - writer.inlineBlock(() => { - writer.write('NOT: '); - exprWriter.write(expr); - }); - writer.conditionalWrite(i !== denyRules.length - 1, ','); - }); - writer.conditionalWrite(denyRules.length > 1, ']}'); - }; - - const writeAllows = () => { - writer.conditionalWrite(allowRules.length > 1, '{ OR: ['); - allowRules.forEach((expr, i) => { - exprWriter.write(expr); - writer.conditionalWrite(i !== allowRules.length - 1, ','); - }); - writer.conditionalWrite(allowRules.length > 1, ']}'); - }; - - if (allowRules.length > 0 && denyRules.length > 0) { - // include both allow and deny rules - writer.write('{ AND: ['); - writeDenies(); - writer.write(','); - writeAllows(); - writer.write(']}'); - } else if (denyRules.length > 0) { - // only deny rules - writeDenies(); - } else if (allowRules.length > 0) { - // only allow rules - writeAllows(); - } else { - // disallow any operation unless for 'postUpdate' - writer.write(`return ${kind === 'postUpdate' ? TRUE : FALSE};`); - } - writer.write(';'); - statements.push(writer.result); - } - - return { - name: getQueryGuardFunctionName(model, forField, fieldOverride, kind), - returnType: 'any', - parameters: [ - { - name: 'context', - type: 'QueryContext', - }, - { - // for generating field references used by field comparison in the same model - name: 'db', - type: 'CrudContract', - }, - ], - statements, - } as OptionalKind; -} - -export function generateEntityCheckerFunction( - model: DataModel, - kind: PolicyOperationKind, - allows: Expression[], - denies: Expression[], - forField?: DataModelField, - fieldOverride = false -) { - const statements: string[] = []; - - generateNormalizedAuthRef(model, allows, denies, statements); - - const transformer = new TypeScriptExpressionTransformer({ - context: ExpressionContext.AccessPolicy, - thisExprContext: 'input', - fieldReferenceContext: 'input', - isPostGuard: kind === 'postUpdate', - futureRefContext: 'input', - operationContext: kind, - }); - - denies.forEach((rule) => { - const compiled = transformer.transform(rule, false); - statements.push(`if (${compiled}) { return false; }`); - }); - - allows.forEach((rule) => { - const compiled = transformer.transform(rule, false); - statements.push(`if (${compiled}) { return true; }`); - }); - - if (kind === 'postUpdate') { - // 'postUpdate' rule defaults to allow - statements.push('return true;'); - } else { - if (forField) { - // if there's no allow rule, for field-level rules, by default we allow - if (allows.length === 0) { - statements.push('return true;'); - } else { - // if there's any allow rule, we deny unless any allow rule evaluates to true - statements.push(`return false;`); - } - } else { - // for other cases, defaults to deny - statements.push(`return false;`); - } - } - - return { - name: getEntityCheckerFunctionName(model, forField, fieldOverride, kind), - returnType: 'any', - parameters: [ - { - name: 'input', - type: 'any', - }, - { - name: 'context', - type: 'QueryContext', - }, - ], - statements, - } as OptionalKind; -} - -/** - * Generates a normalized auth reference for the given policy rules - */ -export function generateNormalizedAuthRef( - model: DataModel, - allows: Expression[], - denies: Expression[], - statements: string[] -) { - // check if any allow or deny rule contains 'auth()' invocation - const hasAuthRef = [...allows, ...denies].some((rule) => streamAst(rule).some((child) => isAuthInvocation(child))); - - if (hasAuthRef) { - const authModel = getAuthDecl(getDataModelAndTypeDefs(model.$container, true)); - if (!authModel) { - throw new PluginError(name, 'Auth model not found'); - } - const userIdFields = getIdFields(authModel); - if (!userIdFields || userIdFields.length === 0) { - throw new PluginError(name, 'User model does not have an id field'); - } - - // normalize user to null to avoid accidentally use undefined in filter - statements.push(`const user: any = context.user ?? null;`); - } -} - -/** - * Check if the given enum is referenced in the model - */ -export function isEnumReferenced(model: Model, decl: Enum): unknown { - const dataModels = getDataModels(model); - return dataModels.some((dm) => { - return streamAllContents(dm).some((node) => { - if (isDataModelField(node) && node.type.reference?.ref === decl) { - // referenced as field type - return true; - } - if (isEnumFieldReference(node) && node.target.ref?.$container === decl) { - // enum field is referenced - return true; - } - return false; - }); - }); -} - -function hasCrossModelComparison(expr: Expression) { - return streamAst(expr).some((node) => { - if (isBinaryExpr(node) && ['==', '!=', '>', '<', '>=', '<=', 'in'].includes(node.operator)) { - const leftRoot = getSourceModelOfFieldAccess(node.left); - const rightRoot = getSourceModelOfFieldAccess(node.right); - if (leftRoot && rightRoot && leftRoot !== rightRoot) { - return true; - } - } - return false; - }); -} - -function getSourceModelOfFieldAccess(expr: Expression) { - // `auth()` access doesn't involve db field look up so doesn't count as cross-model comparison - if (isAuthInvocation(expr)) { - return undefined; - } - - // an expression that resolves to a data model and is part of a member access, return the model - // e.g.: profile.age => Profile - if (isDataModel(expr.$resolvedType?.decl) && isMemberAccessExpr(expr.$container)) { - return expr.$resolvedType?.decl; - } - - // `this` reference - if (isThisExpr(expr)) { - return getContainerOfType(expr, isDataModel); - } - - // `future()` - if (isFutureInvocation(expr)) { - return getContainerOfType(expr, isDataModel); - } - - // direct field reference, return the model - if (isDataModelFieldReference(expr)) { - return (expr.target.ref as DataModelField).$container; - } - - // member access - if (isMemberAccessExpr(expr)) { - return getSourceModelOfFieldAccess(expr.operand); - } - - return undefined; -} diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts deleted file mode 100644 index 3510b0979..000000000 --- a/packages/schema/src/plugins/plugin-utils.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { DEFAULT_RUNTIME_LOAD_PATH, type PolicyOperationKind } from '@zenstackhq/runtime'; -import { ensureEmptyDir, getLiteral, PluginGlobalOptions } from '@zenstackhq/sdk'; -import { isPlugin, Model, Plugin } from '@zenstackhq/sdk/ast'; -import fs from 'fs'; -import path from 'path'; -import { PluginRunnerOptions } from '../cli/plugin-runner'; -import { getVersion } from '../utils/version-utils'; - -export const ALL_OPERATION_KINDS: PolicyOperationKind[] = ['create', 'update', 'postUpdate', 'read', 'delete']; - -/** - * Gets the nearest "node_modules" folder by walking up from start path. - */ -export function getNodeModulesFolder(startPath?: string): string | undefined { - startPath = startPath ?? process.cwd(); - if (startPath.endsWith('node_modules')) { - return startPath; - } else if (fs.existsSync(path.join(startPath, 'node_modules'))) { - return path.join(startPath, 'node_modules'); - } else if (startPath !== '/') { - const parent = path.join(startPath, '..'); - return getNodeModulesFolder(parent); - } else { - return undefined; - } -} - -/** - * Ensure the default output folder is initialized. - */ -export function ensureDefaultOutputFolder(options: PluginRunnerOptions) { - const output = options.output ? path.resolve(options.output) : getDefaultOutputFolder(); - if (output) { - ensureEmptyDir(output); - if (!options.output) { - const pkgJson = { - name: 'zenstack-generated', - version: getVersion() ?? '1.0.0', - exports: { - './enhance': { - types: './enhance.d.ts', - default: './enhance.js', - }, - './enhance-edge': { - types: './enhance-edge.d.ts', - default: './enhance-edge.js', - }, - './zod': { - types: './zod/index.d.ts', - default: './zod/index.js', - }, - './zod/models': { - types: './zod/models/index.d.ts', - default: './zod/models/index.js', - }, - './zod/input': { - types: './zod/input/index.d.ts', - default: './zod/input/index.js', - }, - './zod/objects': { - types: './zod/objects/index.d.ts', - default: './zod/objects/index.js', - }, - './model-meta': { - types: './model-meta.d.ts', - default: './model-meta.js', - }, - './models': { - types: './models.d.ts', - default: './models.js', - }, - }, - }; - - // create stubs for zod exports to make bundlers that statically - // analyze imports (like Next.js) happy - for (const zodFolder of ['models', 'input', 'objects']) { - fs.mkdirSync(path.join(output, 'zod', zodFolder), { recursive: true }); - fs.writeFileSync(path.join(output, 'zod', zodFolder, 'index.js'), ''); - } - - fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkgJson, undefined, 4)); - } - } - - return output; -} - -/** - * Gets the default node_modules/.zenstack output folder for plugins. - * @returns - */ -export function getDefaultOutputFolder(globalOptions?: PluginGlobalOptions) { - if (typeof globalOptions?.output === 'string') { - return path.resolve(globalOptions.output); - } - - // for testing, use the local node_modules - if (process.env.ZENSTACK_TEST === '1') { - return path.join(process.cwd(), 'node_modules', DEFAULT_RUNTIME_LOAD_PATH); - } - - // find the real runtime module path, it might be a symlink in pnpm - let runtimeModulePath = require.resolve('@zenstackhq/runtime'); - - // start with the parent folder of @zenstackhq, supposed to be a node_modules folder - while (!runtimeModulePath.endsWith('@zenstackhq') && runtimeModulePath !== '/') { - runtimeModulePath = path.join(runtimeModulePath, '..'); - } - runtimeModulePath = path.join(runtimeModulePath, '..'); - - const modulesFolder = getNodeModulesFolder(runtimeModulePath); - return modulesFolder ? path.join(modulesFolder, DEFAULT_RUNTIME_LOAD_PATH) : undefined; -} - -/** - * Core plugin providers - */ -export enum CorePlugins { - Prisma = '@core/prisma', - Zod = '@core/zod', - Enhancer = '@core/enhancer', -} - -/** - * Gets the custom output folder for a plugin. - */ -export function getPluginCustomOutputFolder(zmodel: Model, provider: string) { - const plugin = zmodel.declarations.find( - (d): d is Plugin => - isPlugin(d) && d.fields.some((f) => f.name === 'provider' && getLiteral(f.value) === provider) - ); - if (!plugin) { - return undefined; - } - const output = plugin.fields.find((f) => f.name === 'output'); - return output && getLiteral(output.value); -} diff --git a/packages/schema/src/plugins/prisma/indent-string.ts b/packages/schema/src/plugins/prisma/indent-string.ts deleted file mode 100644 index 8b61750dd..000000000 --- a/packages/schema/src/plugins/prisma/indent-string.ts +++ /dev/null @@ -1,9 +0,0 @@ -// https://github.com/sindresorhus/indent-string - -/** - * Utility for indenting strings - */ -export default function indentString(string: string, count = 4): string { - const indent = ' '; - return string.replace(/^(?!\s*$)/gm, indent.repeat(count)); -} diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts deleted file mode 100644 index 85832b266..000000000 --- a/packages/schema/src/plugins/prisma/index.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { - PluginError, - type PluginFunction, - type PluginOptions, - getPrismaClientGenerator, - normalizedRelative, - resolvePath, -} from '@zenstackhq/sdk'; -import { getDMMF } from '@zenstackhq/sdk/prisma'; -import colors from 'colors'; -import fs from 'fs'; -import path from 'path'; -import stripColor from 'strip-color'; -import telemetry from '../../telemetry'; -import { execPackage } from '../../utils/exec-utils'; -import { findUp } from '../../utils/pkg-utils'; -import { PrismaSchemaGenerator } from './schema-generator'; - -export const name = 'Prisma'; -export const description = 'Generating Prisma schema'; - -const run: PluginFunction = async (model, options, _dmmf, _globalOptions) => { - // deal with calculation of the default output location - const output = options.output - ? resolvePath(options.output as string, options) - : getDefaultPrismaOutputFile(options.schemaPath); - - const mergedOptions = { ...options, output } as unknown as PluginOptions; - const { warnings, shortNameMap } = await new PrismaSchemaGenerator(model).generate(mergedOptions); - - // the path to import the prisma client from - let prismaClientPath = '@prisma/client'; - - // the real path where the prisma client was generated - let clientOutputDir = '.prisma/client'; - - // the path to the prisma client dts file - let prismaClientDtsPath: string | undefined = undefined; - - if (options.generateClient !== false) { - let generateCmd = `prisma generate --schema "${output}"`; - if (typeof options.generateArgs === 'string') { - generateCmd += ` ${options.generateArgs}`; - } - try { - // run 'prisma generate' - await execPackage(generateCmd, { stdio: 'ignore' }); - } catch { - await trackPrismaSchemaError(output); - try { - // run 'prisma generate' again with output to the console - await execPackage(generateCmd); - } catch { - // noop - } - throw new PluginError(name, `Failed to run "prisma generate"`); - } - - // extract user-provided prisma client output path - const gen = getPrismaClientGenerator(model); - const clientOutput = gen?.output; - const newGenerator = !!gen?.isNewGenerator; - - if (clientOutput) { - if (path.isAbsolute(clientOutput)) { - prismaClientPath = clientOutput; - } else { - // first get absolute path based on prisma schema location - const absPath = path.resolve(path.dirname(output), clientOutput); - - // then make it relative to the zmodel schema location - prismaClientPath = normalizedRelative(path.dirname(options.schemaPath), absPath); - } - - // record custom location where the prisma client was generated - clientOutputDir = prismaClientPath; - } - - if (newGenerator) { - // "prisma-client" generator requires an extra "/client" import suffix - prismaClientPath = `${prismaClientPath}/client`; - } - - // get PrismaClient dts path - - if (clientOutput) { - // if a custom prisma client output path is configured, first try treating - // clientOutputDir as a relative path and locate the index.d.ts file - prismaClientDtsPath = path.resolve(path.dirname(options.schemaPath), clientOutputDir, 'index.d.ts'); - } - - if (!newGenerator && (!prismaClientDtsPath || !fs.existsSync(prismaClientDtsPath))) { - // if the file does not exist, try node module resolution - try { - // the resolution is relative to the schema path by default - let resolveBase = path.dirname(options.schemaPath); - if (!clientOutput) { - // PrismaClient is generated into the default location, considering symlinked - // environments like pnpm, we need to first resolve "@prisma/client",and then - // resolve the ".prisma/client/index.d.ts" file relative to that - resolveBase = path.dirname(require.resolve('@prisma/client', { paths: [resolveBase] })); - } - const prismaClientResolvedPath = require.resolve(clientOutputDir, { paths: [resolveBase] }); - prismaClientDtsPath = path.join(path.dirname(prismaClientResolvedPath), 'index.d.ts'); - } catch (err) { - console.warn( - colors.yellow( - `Could not resolve PrismaClient type declaration path. This may break plugins that depend on it.` - ) - ); - } - } - } else { - console.warn( - colors.yellow( - 'Skipping prisma client generation because "generateClient" is set to false. This may break plugins that depend on the prisma client.' - ) - ); - } - - // load the result DMMF - const dmmf = await getDMMF({ - datamodel: fs.readFileSync(output, 'utf-8'), - }); - - return { warnings, dmmf, prismaClientPath, prismaClientDtsPath, shortNameMap }; -}; - -function getDefaultPrismaOutputFile(schemaPath: string) { - // handle override from package.json - const pkgJsonPath = findUp(['package.json'], path.dirname(schemaPath)); - if (pkgJsonPath) { - const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); - if (typeof pkgJson?.zenstack?.prisma === 'string') { - if (path.isAbsolute(pkgJson.zenstack.prisma)) { - return pkgJson.zenstack.prisma; - } else { - // resolve relative to package.json - return path.resolve(path.dirname(pkgJsonPath), pkgJson.zenstack.prisma); - } - } - } - - return resolvePath('./prisma/schema.prisma', { schemaPath }); -} - -export async function trackPrismaSchemaError(schema: string) { - try { - await getDMMF({ datamodel: fs.readFileSync(schema, 'utf-8') }); - } catch (err) { - if (err instanceof Error) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - telemetry.track('prisma:error', { command: 'generate', message: stripColor(err.message) }); - } - } -} - -export default run; diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts deleted file mode 100644 index 594913f8c..000000000 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ /dev/null @@ -1,379 +0,0 @@ -import indentString from './indent-string'; - -/** - * Field used by datasource and generator declarations. - */ -export type SimpleField = { name: string; text: string }; - -/** - * Prisma schema builder - */ -export class PrismaModel { - private datasources: DataSource[] = []; - private generators: Generator[] = []; - private models: Model[] = []; - private enums: Enum[] = []; - - addDataSource(name: string, fields: SimpleField[] = []): DataSource { - const ds = new DataSource(name, fields); - this.datasources.push(ds); - return ds; - } - - addGenerator(name: string, fields: SimpleField[]): Generator { - const generator = new Generator(name, fields); - this.generators.push(generator); - return generator; - } - - addModel(name: string): Model { - const model = new Model(name, false); - this.models.push(model); - return model; - } - - addView(name: string): Model { - const model = new Model(name, true); - this.models.push(model); - return model; - } - - addEnum(name: string): Enum { - const e = new Enum(name); - this.enums.push(e); - return e; - } - - toString(): string { - 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[] = []) {} - - toString(): string { - return ( - `datasource ${this.name} {\n` + - this.fields.map((f) => indentString(`${f.name} = ${f.text}`)).join('\n') + - `\n}` - ); - } -} - -export class Generator { - 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') + - `\n}` - ); - } -} - -export class DeclarationBase { - constructor(public documentations: string[] = []) {} - - addComment(name: string): string { - this.documentations.push(name); - return name; - } - - toString(): string { - return this.documentations.map((x) => `${x}\n`).join(''); - } -} - -export class ContainerDeclaration extends DeclarationBase { - constructor(documentations: string[] = [], public attributes: (ContainerAttribute | PassThroughAttribute)[] = []) { - super(documentations); - } -} - -export class FieldDeclaration extends DeclarationBase { - constructor(documentations: string[] = [], public attributes: (FieldAttribute | PassThroughAttribute)[] = []) { - super(documentations); - } -} - -export class Model extends ContainerDeclaration { - public fields: ModelField[] = []; - constructor(public name: string, public isView: boolean, documentations: string[] = []) { - super(documentations); - } - - addField( - name: string, - type: ModelFieldType | string, - attributes: (FieldAttribute | PassThroughAttribute)[] = [], - documentations: string[] = [], - addToFront = false - ): ModelField { - const field = new ModelField(name, type, attributes, documentations); - if (addToFront) { - this.fields.unshift(field); - } else { - this.fields.push(field); - } - return field; - } - - addAttribute(name: string, args: AttributeArg[] = []) { - const attr = new ContainerAttribute(name, args); - this.attributes.push(attr); - return attr; - } - - toString(): string { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result: any[] = [...this.fields]; - - if (this.attributes.length > 0) { - // Add a blank line before the attributes - result.push(''); - } - - result.push(...this.attributes); - - return ( - super.toString() + - `${this.isView ? 'view' : 'model'} ${this.name} {\n` + - indentString(result.map((d) => d.toString()).join('\n')) + - `\n}` - ); - } -} - -export type ScalarTypes = - | 'String' - | 'Boolean' - | 'Int' - | 'BigInt' - | 'Float' - | 'Decimal' - | 'DateTime' - | 'Json' - | 'Bytes' - | 'Unsupported'; - -export class ModelFieldType { - constructor(public type: ScalarTypes | string, public array?: boolean, public optional?: boolean) {} - - toString(): string { - return `${this.type}${this.array ? '[]' : ''}${this.optional ? '?' : ''}`; - } -} - -export class ModelField extends FieldDeclaration { - constructor( - public name: string, - public type: ModelFieldType | string, - attributes: (FieldAttribute | PassThroughAttribute)[] = [], - documentations: string[] = [] - ) { - super(documentations, attributes); - } - - addAttribute(name: string, args: AttributeArg[] = []): FieldAttribute { - const attr = new FieldAttribute(name, args); - this.attributes.push(attr); - return attr; - } - - toString(): string { - return ( - super.toString() + - `${this.name} ${this.type}` + - (this.attributes.length > 0 ? ' ' + this.attributes.map((a) => a.toString()).join(' ') : '') - ); - } -} - -export class FieldAttribute { - constructor(public name: string, public args: AttributeArg[] = []) {} - - toString(): string { - return `${this.name}(` + this.args.map((a) => a.toString()).join(', ') + `)`; - } -} - -export class ContainerAttribute { - constructor(public name: string, public args: AttributeArg[] = []) {} - - toString(): string { - return `${this.name}(` + this.args.map((a) => a.toString()).join(', ') + `)`; - } -} - -/** - * Represents @@prisma.passthrough and @prisma.passthrough - */ -export class PassThroughAttribute { - constructor(public text: string) {} - - toString(): string { - return this.text; - } -} - -export class AttributeArg { - constructor(public name: string | undefined, public value: AttributeArgValue) {} - - toString(): string { - 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[] - ) { - switch (type) { - case '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'); - break; - case 'Array': - if (!Array.isArray(value)) throw new Error('Value must be array'); - break; - case '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'); - break; - } - } - - toString(): string { - switch (this.type) { - case 'String': - // use JSON.stringify to escape quotes - return JSON.stringify(this.value); - case 'Number': - return this.value.toString(); - case 'FieldReference': { - if (typeof this.value === 'string') { - return this.value; - } else { - const fr = this.value as FieldReference; - let r = fr.field; - if (fr.args.length > 0) { - r += '(' + fr.args.map((a) => a.toString()).join(',') + ')'; - } - return r; - } - } - case 'FunctionCall': - return this.value.toString(); - case 'Boolean': - return this.value ? 'true' : 'false'; - case 'Array': - return '[' + (this.value as AttributeArgValue[]).map((v) => v.toString()).join(', ') + ']'; - default: - throw new Error(`Unknown attribute value type ${this.type}`); - } - } -} - -export class FieldReference { - constructor(public field: string, public args: FieldReferenceArg[] = []) {} -} - -export class FieldReferenceArg { - constructor(public name: string, public value: string) {} - - toString(): string { - return `${this.name}: ${this.value}`; - } -} - -export class FunctionCall { - constructor(public func: string, public args: FunctionCallArg[] = []) {} - - toString(): string { - return `${this.func}` + '(' + this.args.map((a) => a.toString()).join(', ') + ')'; - } -} - -export class FunctionCallArg { - constructor(public value: string) {} - - toString(): string { - return this.value; - } -} - -export class Enum extends ContainerDeclaration { - public fields: EnumField[] = []; - - constructor(public name: string, public documentations: string[] = []) { - super(documentations); - } - - addField( - name: string, - attributes: (FieldAttribute | PassThroughAttribute)[] = [], - documentations: string[] = [] - ): EnumField { - const field = new EnumField(name, attributes, documentations); - this.fields.push(field); - return field; - } - - addAttribute(name: string, args: AttributeArg[] = []) { - const attr = new ContainerAttribute(name, args); - this.attributes.push(attr); - return attr; - } - - addComment(name: string): string { - this.documentations.push(name); - return name; - } - - toString(): string { - return ( - super.toString() + - `enum ${this.name} {\n` + - indentString([...this.fields, ...this.attributes].map((d) => d.toString()).join('\n')) + - '\n}' - ); - } -} - -export class EnumField extends DeclarationBase { - constructor( - public name: string, - public attributes: (FieldAttribute | PassThroughAttribute)[] = [], - public documentations: string[] = [] - ) { - super(); - } - - addAttribute(name: string, args: AttributeArg[] = []): FieldAttribute { - const attr = new FieldAttribute(name, args); - this.attributes.push(attr); - return attr; - } - - toString(): string { - return ( - super.toString() + - this.name + - (this.attributes.length > 0 ? ' ' + this.attributes.map((a) => a.toString()).join(' ') : '') - ); - } -} diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts deleted file mode 100644 index 4e29bb91d..000000000 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ /dev/null @@ -1,1044 +0,0 @@ -import { - AttributeArg, - BooleanLiteral, - ConfigArrayExpr, - ConfigExpr, - ConfigInvocationArg, - DataModel, - DataModelAttribute, - DataModelField, - DataModelFieldAttribute, - DataModelFieldType, - DataSource, - Enum, - EnumField, - Expression, - GeneratorDecl, - InvocationExpr, - isArrayExpr, - isDataModel, - isDataSource, - isInvocationExpr, - isLiteralExpr, - isNullExpr, - isReferenceExpr, - isStringLiteral, - isTypeDef, - LiteralExpr, - Model, - NumberLiteral, - StringLiteral, -} from '@zenstackhq/language/ast'; -import { - getAttribute, - getAttributeArg, - getAttributeArgLiteral, - getDataSourceProvider, - getIdFields, - getInheritedFromDelegate, - getLiteral, - getRelationKeyPairs, - isDelegateModel, - isIdField, - PluginError, - PluginOptions, - resolved, - ZModelCodeGenerator, -} from '@zenstackhq/sdk'; -import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; -import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; -import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import fs from 'fs'; -import { writeFile } from 'fs/promises'; -import path from 'path'; -import semver from 'semver'; -import { match, P } from 'ts-pattern'; -import { name } from '.'; -import { getStringLiteral } from '../../language-server/validator/utils'; -import { getConcreteModels } from '../../utils/ast-utils'; -import { execPackage } from '../../utils/exec-utils'; -import { isDefaultWithAuth } from '../enhancer/enhancer-utils'; -import { - AttributeArgValue, - ModelField, - ModelFieldType, - AttributeArg as PrismaAttributeArg, - AttributeArgValue as PrismaAttributeArgValue, - ContainerDeclaration as PrismaContainerDeclaration, - Model as PrismaDataModel, - Enum as PrismaEnum, - FieldAttribute as PrismaFieldAttribute, - FieldReference as PrismaFieldReference, - FieldReferenceArg as PrismaFieldReferenceArg, - FunctionCall as PrismaFunctionCall, - FunctionCallArg as PrismaFunctionCallArg, - PrismaModel, - ContainerAttribute as PrismaModelAttribute, - PassThroughAttribute as PrismaPassThroughAttribute, - SimpleField, -} from './prisma-builder'; - -const MODEL_PASSTHROUGH_ATTR = '@@prisma.passthrough'; -const FIELD_PASSTHROUGH_ATTR = '@prisma.passthrough'; -const PROVIDERS_SUPPORTING_NAMED_CONSTRAINTS = ['postgresql', 'mysql', 'cockroachdb']; -const PROVIDERS_SUPPORTING_TYPEDEF_FIELDS = ['postgresql', 'sqlite']; - -// Some database providers like postgres and mysql have default limit to the length of identifiers -// Here we use a conservative value that should work for most cases, and truncate names if needed -const IDENTIFIER_NAME_MAX_LENGTH = 50 - DELEGATE_AUX_RELATION_PREFIX.length; - -/** - * Generates Prisma schema file - */ -export class PrismaSchemaGenerator { - private zModelGenerator: ZModelCodeGenerator = new ZModelCodeGenerator(); - - private readonly PRELUDE = `////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -////////////////////////////////////////////////////////////////////////////////////////////// - -`; - - private mode: 'logical' | 'physical' = 'physical'; - private customAttributesAsComments = false; - - // a mapping from full names to shortened names - private shortNameMap = new Map(); - - constructor(private readonly zmodel: Model) {} - - async generate(options: PluginOptions) { - if (!options.output) { - throw new PluginError(name, 'Output file is not specified'); - } - - const outFile = options.output as string; - const warnings: string[] = []; - if (options.mode) { - this.mode = options.mode as 'logical' | 'physical'; - } - - if ( - options.customAttributesAsComments !== undefined && - typeof options.customAttributesAsComments !== 'boolean' - ) { - throw new PluginError(name, 'option "customAttributesAsComments" must be a boolean'); - } - this.customAttributesAsComments = options.customAttributesAsComments === true; - - const prismaVersion = getPrismaVersion(); - if (prismaVersion && semver.lt(prismaVersion, PRISMA_MINIMUM_VERSION)) { - warnings.push( - `ZenStack requires Prisma version "${PRISMA_MINIMUM_VERSION}" or higher. Detected version is "${prismaVersion}".` - ); - } - - const prisma = new PrismaModel(); - - for (const decl of this.zmodel.declarations) { - switch (decl.$type) { - case DataSource: - this.generateDataSource(prisma, decl as DataSource); - break; - - case Enum: - this.generateEnum(prisma, decl as Enum); - break; - - case DataModel: - this.generateModel(prisma, decl as DataModel); - break; - - case GeneratorDecl: - this.generateGenerator(prisma, decl as GeneratorDecl, options); - break; - } - } - - if (!fs.existsSync(path.dirname(outFile))) { - fs.mkdirSync(path.dirname(outFile), { recursive: true }); - } - await writeFile(outFile, this.PRELUDE + prisma.toString()); - - if (options.format !== false) { - try { - // run 'prisma format' - await execPackage(`prisma format --schema ${outFile}`, { stdio: 'ignore' }); - } catch { - warnings.push(`Failed to format Prisma schema file`); - } - } - - return { warnings, shortNameMap: this.shortNameMap }; - } - - private generateDataSource(prisma: PrismaModel, dataSource: DataSource) { - const fields: SimpleField[] = dataSource.fields.map((f) => ({ - name: f.name, - text: this.configExprToText(f.value), - })); - prisma.addDataSource(dataSource.name, fields); - } - - private configExprToText(expr: ConfigExpr) { - if (isLiteralExpr(expr)) { - return this.literalToText(expr); - } else if (isInvocationExpr(expr)) { - const fc = this.makeFunctionCall(expr); - return fc.toString(); - } else { - return this.configArrayToText(expr); - } - } - - private configArrayToText(expr: ConfigArrayExpr) { - return ( - '[' + - expr.items - .map((item) => { - if (isLiteralExpr(item)) { - return this.literalToText(item); - } else { - return ( - item.name + - (item.args.length > 0 - ? '(' + item.args.map((arg) => this.configInvocationArgToText(arg)).join(', ') + ')' - : '') - ); - } - }) - .join(', ') + - ']' - ); - } - - private configInvocationArgToText(arg: ConfigInvocationArg) { - return `${arg.name}: ${this.literalToText(arg.value)}`; - } - - private literalToText(expr: LiteralExpr) { - return JSON.stringify(expr.value); - } - - private exprToText(expr: Expression) { - return new ZModelCodeGenerator({ quote: 'double' }).generate(expr); - } - - private generateGenerator(prisma: PrismaModel, decl: GeneratorDecl, options: PluginOptions) { - const generator = prisma.addGenerator( - decl.name, - decl.fields.map((f) => ({ name: f.name, text: this.configExprToText(f.value) })) - ); - - // deal with configuring PrismaClient preview features - const provider = generator.fields.find((f) => f.name === 'provider'); - if ( - provider?.text === JSON.stringify('prisma-client-js') || - provider?.text === JSON.stringify('prisma-client') - ) { - const prismaVersion = getPrismaVersion(); - if (prismaVersion) { - const previewFeatures = JSON.parse( - generator.fields.find((f) => f.name === 'previewFeatures')?.text ?? '[]' - ); - - if (!Array.isArray(previewFeatures)) { - throw new PluginError(name, 'option "previewFeatures" must be an array'); - } - - if (previewFeatures.length > 0) { - const curr = generator.fields.find((f) => f.name === 'previewFeatures'); - if (!curr) { - generator.fields.push({ name: 'previewFeatures', text: JSON.stringify(previewFeatures) }); - } else { - curr.text = JSON.stringify(previewFeatures); - } - } - } - - if (typeof options.overrideClientGenerationPath === 'string') { - const output = generator.fields.find((f) => f.name === 'output'); - if (output) { - output.text = JSON.stringify(options.overrideClientGenerationPath); - } else { - generator.fields.push({ - name: 'output', - text: JSON.stringify(options.overrideClientGenerationPath), - }); - } - } - } - } - - private generateModel(prisma: PrismaModel, decl: DataModel) { - const model = decl.isView ? prisma.addView(decl.name) : prisma.addModel(decl.name); - for (const field of decl.fields) { - if (field.$inheritedFrom) { - const inheritedFromDelegate = getInheritedFromDelegate(field); - if ( - // fields inherited from delegate are excluded from physical schema - !inheritedFromDelegate || - // logical schema keeps all inherited fields - this.mode === 'logical' || - // id fields are always kept - isIdField(field) - ) { - this.generateModelField(model, field); - } - } else { - this.generateModelField(model, field); - } - } - - for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) { - this.generateContainerAttribute(model, attr); - } - - // user defined comments pass-through - decl.comments.forEach((c) => model.addComment(c)); - this.getCustomAttributesAsComments(decl).forEach((c) => model.addComment(c)); - - // physical: generate relation fields on base models linking to concrete models - this.generateDelegateRelationForBase(model, decl); - - // physical: generate reverse relation fields on concrete models - this.generateDelegateRelationForConcrete(model, decl); - - // logical: expand relations on other models that reference delegated models to concrete models - this.expandPolymorphicRelations(model, decl); - - // logical: ensure relations inherited from delegate models - this.ensureRelationsInheritedFromDelegate(model, decl); - } - - private generateDelegateRelationForBase(model: PrismaDataModel, decl: DataModel) { - if (this.mode !== 'physical') { - return; - } - - if (!isDelegateModel(decl)) { - return; - } - - // collect concrete models inheriting this model - const concreteModels = getConcreteModels(decl); - - // generate an optional relation field in delegate base model to each concrete model - concreteModels.forEach((concrete) => { - const auxName = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(concrete.name)}`); - model.addField(auxName, new ModelFieldType(concrete.name, false, true)); - }); - } - - private generateDelegateRelationForConcrete(model: PrismaDataModel, concreteDecl: DataModel) { - if (this.mode !== 'physical') { - return; - } - - // generate a relation field for each delegated base model - - const baseModels = concreteDecl.superTypes - .map((t) => t.ref) - .filter((t): t is DataModel => !!t) - .filter((t) => isDelegateModel(t)); - - baseModels.forEach((base) => { - const idFields = getIdFields(base); - - // add relation fields - const relationField = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(base.name)}`); - model.addField(relationField, base.name, [ - new PrismaFieldAttribute('@relation', [ - new PrismaAttributeArg( - 'fields', - new AttributeArgValue( - 'Array', - idFields.map( - (idField) => - new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name)) - ) - ) - ), - new PrismaAttributeArg( - 'references', - new AttributeArgValue( - 'Array', - idFields.map( - (idField) => - new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name)) - ) - ) - ), - new PrismaAttributeArg( - 'onDelete', - new AttributeArgValue('FieldReference', new PrismaFieldReference('Cascade')) - ), - new PrismaAttributeArg( - 'onUpdate', - new AttributeArgValue('FieldReference', new PrismaFieldReference('Cascade')) - ), - ]), - ]); - }); - } - - private expandPolymorphicRelations(model: PrismaDataModel, dataModel: DataModel) { - if (this.mode !== 'logical') { - return; - } - - // the logical schema needs to expand relations to the delegate models to concrete ones - - // for the given model, find relation fields of delegate model type, find all concrete models - // of the delegate model and generate an auxiliary opposite relation field to each of them - dataModel.fields.forEach((field) => { - // don't process fields inherited from a delegate model - if (field.$inheritedFrom && isDelegateModel(field.$inheritedFrom)) { - return; - } - - const fieldType = field.type.reference?.ref; - if (!isDataModel(fieldType)) { - return; - } - - // find concrete models that inherit from this field's model type - const concreteModels = dataModel.$container.declarations.filter( - (d): d is DataModel => isDataModel(d) && isDescendantOf(d, fieldType) - ); - - concreteModels.forEach((concrete) => { - // aux relation name format: delegate_aux_[model]_[relationField]_[concrete] - // e.g., delegate_aux_User_myAsset_Video - const auxRelationName = this.truncate( - `${DELEGATE_AUX_RELATION_PREFIX}_${dataModel.name}_${field.name}_${concrete.name}` - ); - const auxRelationField = model.addField( - auxRelationName, - new ModelFieldType(concrete.name, field.type.array, field.type.optional) - ); - - const relAttr = getAttribute(field, '@relation'); - let relAttrAdded = false; - if (relAttr) { - if (getAttributeArg(relAttr, 'fields')) { - // for reach foreign key field pointing to the delegate model, we need to create an aux foreign key - // to point to the concrete model - const relationFieldPairs = getRelationKeyPairs(field); - const addedFkFields: ModelField[] = []; - for (const { foreignKey } of relationFieldPairs) { - const addedFkField = this.replicateForeignKey(model, dataModel, concrete, foreignKey); - addedFkFields.push(addedFkField); - } - - // the `@relation(..., fields: [...])` attribute argument - const fieldsArg = new AttributeArgValue( - 'Array', - addedFkFields.map( - (addedFk) => - new AttributeArgValue('FieldReference', new PrismaFieldReference(addedFk.name)) - ) - ); - - // the `@relation(..., references: [...])` attribute argument - const referencesArg = new AttributeArgValue( - 'Array', - relationFieldPairs.map( - ({ id }) => new AttributeArgValue('FieldReference', new PrismaFieldReference(id.name)) - ) - ); - - const addedRel = new PrismaFieldAttribute('@relation', [ - // use field name as relation name for disambiguation - new PrismaAttributeArg(undefined, new AttributeArgValue('String', auxRelationField.name)), - new PrismaAttributeArg('fields', fieldsArg), - new PrismaAttributeArg('references', referencesArg), - ]); - - if (this.supportNamedConstraints) { - addedRel.args.push( - // generate a `map` argument for foreign key constraint disambiguation - new PrismaAttributeArg( - 'map', - new PrismaAttributeArgValue('String', `${auxRelationField.name}_fk`) - ) - ); - } - auxRelationField.attributes.push(addedRel); - relAttrAdded = true; - } - } - - if (!relAttrAdded) { - auxRelationField.attributes.push( - new PrismaFieldAttribute('@relation', [ - // use field name as relation name for disambiguation - new PrismaAttributeArg(undefined, new AttributeArgValue('String', auxRelationField.name)), - ]) - ); - } - }); - }); - } - - private replicateForeignKey( - model: PrismaDataModel, - delegateModel: DataModel, - concreteModel: DataModel, - origForeignKey: DataModelField - ) { - // aux fk name format: delegate_aux_[model]_[fkField]_[concrete] - // e.g., delegate_aux_User_myAssetId_Video - - // generate a fk field based on the original fk field - const addedFkField = this.generateModelField(model, origForeignKey); - - // `@map` attribute should not be inherited - addedFkField.attributes = addedFkField.attributes.filter((attr) => !('name' in attr && attr.name === '@map')); - - // `@unique` attribute should be recreated with disambiguated name - addedFkField.attributes = addedFkField.attributes.filter( - (attr) => !('name' in attr && attr.name === '@unique') - ); - const uniqueAttr = addedFkField.addAttribute('@unique'); - const constraintName = this.truncate(`${delegateModel.name}_${addedFkField.name}_${concreteModel.name}_unique`); - uniqueAttr.args.push(new PrismaAttributeArg('map', new AttributeArgValue('String', constraintName))); - - // fix its name - const addedFkFieldName = `${delegateModel.name}_${origForeignKey.name}_${concreteModel.name}`; - addedFkField.name = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${addedFkFieldName}`); - - // we also need to go through model-level `@@unique` and replicate those involving fk fields - this.replicateForeignKeyModelLevelUnique(model, delegateModel, origForeignKey, addedFkField); - - return addedFkField; - } - - private replicateForeignKeyModelLevelUnique( - model: PrismaDataModel, - dataModel: DataModel, - origForeignKey: DataModelField, - addedFkField: ModelField - ) { - for (const uniqueAttr of dataModel.attributes.filter((attr) => attr.decl.ref?.name === '@@unique')) { - const fields = getAttributeArg(uniqueAttr, 'fields'); - if (fields && isArrayExpr(fields)) { - const found = fields.items.find( - (fieldRef) => isReferenceExpr(fieldRef) && fieldRef.target.ref === origForeignKey - ); - if (found) { - // replicate the attribute and replace the field reference with the new FK field - const args: PrismaAttributeArgValue[] = []; - const fieldNames: string[] = []; - for (const arg of fields.items) { - if (!isReferenceExpr(arg)) { - throw new PluginError(name, 'Unexpected field reference in @@unique attribute'); - } - - if (arg.target.ref === origForeignKey) { - // replace - fieldNames.push(addedFkField.name); - args.push( - new PrismaAttributeArgValue( - 'FieldReference', - new PrismaFieldReference(addedFkField.name) - ) - ); - } else { - // copy - fieldNames.push(arg.target.$refText); - args.push( - new PrismaAttributeArgValue( - 'FieldReference', - new PrismaFieldReference(arg.target.$refText) - ) - ); - } - } - - const constraintName = this.truncate(`${dataModel.name}_${fieldNames.join('_')}_unique`); - - model.addAttribute('@@unique', [ - new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('Array', args)), - new PrismaAttributeArg('map', new PrismaAttributeArgValue('String', constraintName)), - ]); - } - } - } - } - - private truncate(name: string) { - if (name.length <= IDENTIFIER_NAME_MAX_LENGTH) { - return name; - } - - const existing = this.shortNameMap.get(name); - if (existing) { - return existing; - } - - const baseName = name.slice(0, IDENTIFIER_NAME_MAX_LENGTH); - let index = 0; - let shortName = `${baseName}_${index}`; - - // eslint-disable-next-line no-constant-condition - while (true) { - const conflict = Array.from(this.shortNameMap.values()).find((v) => v === shortName); - if (!conflict) { - this.shortNameMap.set(name, shortName); - break; - } - - // try next index - index++; - shortName = `${baseName}_${index}`; - } - - return shortName; - } - - private ensureRelationsInheritedFromDelegate(model: PrismaDataModel, decl: DataModel) { - if (this.mode !== 'logical') { - return; - } - - decl.fields.forEach((f) => { - if (!isDataModel(f.type.reference?.ref)) { - // only process relation fields - return; - } - - if (!f.$inheritedFrom) { - // only process inherited fields - return; - } - - // Walk up the inheritance chain to find a field with matching name - // which is where this field is inherited from. - // - // Note that we can't walk all the way up to the $inheritedFrom model - // because it may have been eliminated because of being abstract. - - const baseField = this.findUpMatchingFieldFromDelegate(decl, f); - if (!baseField) { - // only process fields inherited from delegate models - return; - } - - const prismaField = model.fields.find((field) => field.name === f.name); - if (!prismaField) { - return; - } - - // find the opposite side of the relation - const oppositeRelationField = this.getOppositeRelationField(f.type.reference.ref, baseField); - if (!oppositeRelationField) { - return; - } - const oppositeRelationAttr = getAttribute(oppositeRelationField, '@relation'); - - const fieldType = f.type.reference.ref; - - // relation name format: delegate_aux_[relationType]_[oppositeRelationField]_[concrete] - const relName = this.truncate( - `${DELEGATE_AUX_RELATION_PREFIX}_${fieldType.name}_${oppositeRelationField.name}_${decl.name}` - ); - - // recreate `@relation` attribute - prismaField.attributes = prismaField.attributes.filter( - (attr) => (attr as PrismaFieldAttribute).name !== '@relation' - ); - - const relKeyPairs = getRelationKeyPairs(f); - - if ( - // array relation doesn't need FK - f.type.array || - // FK field is defined on this side - relKeyPairs.length > 0 || - // opposite relation already has FK, we don't need to generate on this side - (oppositeRelationAttr && getAttributeArg(oppositeRelationAttr, 'fields')) - ) { - const relationArgs = [new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName))]; - const isSelfRelation = f.type.reference.ref === (f.$inheritedFrom ?? f.$container); - if (relKeyPairs.length > 0 && !isSelfRelation) { - // carry over "fields" and "references" args if not a self-relation - relationArgs.push( - new PrismaAttributeArg( - 'fields', - new AttributeArgValue( - 'Array', - relKeyPairs.map( - (pair) => - new AttributeArgValue( - 'FieldReference', - new PrismaFieldReference(pair.foreignKey.name) - ) - ) - ) - ) - ); - relationArgs.push( - new PrismaAttributeArg( - 'references', - new AttributeArgValue( - 'Array', - relKeyPairs.map( - (pair) => - new AttributeArgValue('FieldReference', new PrismaFieldReference(pair.id.name)) - ) - ) - ) - ); - } - prismaField.attributes.push(new PrismaFieldAttribute('@relation', relationArgs)); - } else { - // generate FK field - const oppositeModelIds = getIdFields(oppositeRelationField.$container as DataModel); - const fkFieldNames: string[] = []; - - oppositeModelIds.forEach((idField) => { - const fkFieldName = this.truncate(`${DELEGATE_AUX_RELATION_PREFIX}_${f.name}_${idField.name}`); - model.addField(fkFieldName, new ModelFieldType(idField.type.type!, false, f.type.optional), [ - // one-to-one relation requires FK field to be unique, we're just including it - // in all cases since it doesn't hurt - new PrismaFieldAttribute('@unique'), - ]); - fkFieldNames.push(fkFieldName); - }); - - prismaField.attributes.push( - new PrismaFieldAttribute('@relation', [ - new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName)), - new PrismaAttributeArg( - 'fields', - new AttributeArgValue( - 'Array', - fkFieldNames.map( - (fk) => new AttributeArgValue('FieldReference', new PrismaFieldReference(fk)) - ) - ) - ), - new PrismaAttributeArg( - 'references', - new AttributeArgValue( - 'Array', - oppositeModelIds.map( - (idField) => - new AttributeArgValue('FieldReference', new PrismaFieldReference(idField.name)) - ) - ) - ), - ]) - ); - } - }); - } - - private findUpMatchingFieldFromDelegate(start: DataModel, target: DataModelField): DataModelField | undefined { - for (const base of start.superTypes) { - if (isDataModel(base.ref)) { - if (isDelegateModel(base.ref)) { - const field = base.ref.fields.find((f) => f.name === target.name); - if (field) { - if (!field.$inheritedFrom || !isDelegateModel(field.$inheritedFrom)) { - // if this field is not inherited from an upper delegate, we're done - return field; - } - } - } - - const upper = this.findUpMatchingFieldFromDelegate(base.ref, target); - if (upper) { - return upper; - } - } - } - return undefined; - } - - private getOppositeRelationField(oppositeModel: DataModel, relationField: DataModelField) { - const relName = this.getRelationName(relationField); - const matches = oppositeModel.fields.filter( - (f) => f.type.reference?.ref === relationField.$container && this.getRelationName(f) === relName - ); - - if (matches.length === 0) { - return undefined; - } else if (matches.length === 1) { - return matches[0]; - } else { - // if there are multiple matches, prefer to use the one with the same field name, - // this can happen with self-relations - const withNameMatch = matches.find((f) => f.name === relationField.name); - if (withNameMatch) { - return withNameMatch; - } else { - return matches[0]; - } - } - } - - private getRelationName(field: DataModelField) { - const relAttr = getAttribute(field, '@relation'); - if (!relAttr) { - return undefined; - } - return getAttributeArgLiteral(relAttr, 'name'); - } - - private get supportNamedConstraints() { - const ds = this.zmodel.declarations.find(isDataSource); - if (!ds) { - return false; - } - - const provider = ds.fields.find((f) => f.name === 'provider'); - if (!provider) { - return false; - } - - const value = getStringLiteral(provider.value); - return value && PROVIDERS_SUPPORTING_NAMED_CONSTRAINTS.includes(value); - } - - private isPrismaAttribute(attr: DataModelAttribute | DataModelFieldAttribute) { - if (!attr.decl.ref) { - return false; - } - const attrDecl = resolved(attr.decl); - return ( - !!attrDecl.attributes.find((a) => a.decl.ref?.name === '@@@prisma') || - // the special pass-through attribute - attrDecl.name === MODEL_PASSTHROUGH_ATTR || - attrDecl.name === FIELD_PASSTHROUGH_ATTR - ); - } - - private getUnsupportedFieldType(fieldType: DataModelFieldType) { - if (fieldType.unsupported) { - const value = getStringLiteral(fieldType.unsupported.value); - if (value) { - return `Unsupported("${value}")`; - } else { - return undefined; - } - } else { - return undefined; - } - } - - private generateModelField(model: PrismaDataModel, field: DataModelField, addToFront = false) { - let fieldType: string | undefined; - - if (field.type.type) { - // intrinsic type - fieldType = field.type.type; - } else if (field.type.reference?.ref) { - // model, enum, or type-def - if (isTypeDef(field.type.reference.ref)) { - this.ensureSupportingTypeDefFields(this.zmodel); - fieldType = 'Json'; - } else { - fieldType = field.type.reference.ref.name; - } - } else { - // Unsupported type - const unsupported = this.getUnsupportedFieldType(field.type); - if (unsupported) { - fieldType = unsupported; - } - } - - if (!fieldType) { - throw new PluginError(name, `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 attributes = field.attributes - .filter((attr) => this.isPrismaAttribute(attr)) - // `@default` with `auth()` is handled outside Prisma - .filter((attr) => !isDefaultWithAuth(attr)) - .filter( - (attr) => - // when building physical schema, exclude `@default` for id fields inherited from delegate base - !( - this.mode === 'physical' && - isIdField(field) && - this.isInheritedFromDelegate(field) && - attr.decl.$refText === '@default' - ) - ) - .map((attr) => this.makeFieldAttribute(attr)); - - // user defined comments pass-through - const docs = [...field.comments, ...this.getCustomAttributesAsComments(field)]; - const result = model.addField(field.name, type, attributes, docs, addToFront); - - if (this.mode === 'logical') { - if (field.attributes.some((attr) => isDefaultWithAuth(attr))) { - // field has `@default` with `auth()`, turn it into a dummy default value, and the - // real default value setting is handled outside Prisma - this.setDummyDefault(result, field); - } - } - - return result; - } - - 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('Boolean', () => new AttributeArgValue('Boolean', false)) - .with('DateTime', () => new AttributeArgValue('FunctionCall', new PrismaFunctionCall('now'))) - .with('Json', () => new AttributeArgValue('String', '{}')) - .with('Bytes', () => new AttributeArgValue('String', '')) - .otherwise(() => { - throw new PluginError(name, `Unsupported field type with default value: ${field.type.type}`); - }); - - result.attributes.push( - new PrismaFieldAttribute('@default', [new PrismaAttributeArg(undefined, dummyDefaultValue)]) - ); - } - - private ensureSupportingTypeDefFields(zmodel: Model) { - const dsProvider = getDataSourceProvider(zmodel); - if (dsProvider && !PROVIDERS_SUPPORTING_TYPEDEF_FIELDS.includes(dsProvider)) { - throw new PluginError(name, `Datasource provider "${dsProvider}" does not support "@json" fields`); - } - } - - private isInheritedFromDelegate(field: DataModelField) { - return field.$inheritedFrom && isDelegateModel(field.$inheritedFrom); - } - - private makeFieldAttribute(attr: DataModelFieldAttribute) { - const attrName = resolved(attr.decl).name; - if (attrName === FIELD_PASSTHROUGH_ATTR) { - const text = getLiteral(attr.args[0].value); - if (text) { - return new PrismaPassThroughAttribute(text); - } else { - throw new PluginError(name, `Invalid arguments for ${FIELD_PASSTHROUGH_ATTR} attribute`); - } - } else { - return new PrismaFieldAttribute( - attrName, - attr.args.map((arg) => this.makeAttributeArg(arg)) - ); - } - } - - private makeAttributeArg(arg: AttributeArg): PrismaAttributeArg { - return new PrismaAttributeArg(arg.name, this.makeAttributeArgValue(arg.value)); - } - - private makeAttributeArgValue(node: Expression): PrismaAttributeArgValue { - if (isLiteralExpr(node)) { - const argType = match(node.$type) - .with(StringLiteral, () => 'String' as const) - .with(NumberLiteral, () => 'Number' as const) - .with(BooleanLiteral, () => 'Boolean' as const) - .exhaustive(); - return new PrismaAttributeArgValue(argType, node.value); - } else if (isArrayExpr(node)) { - return new PrismaAttributeArgValue( - 'Array', - new Array(...node.items.map((item) => this.makeAttributeArgValue(item))) - ); - } else if (isReferenceExpr(node)) { - return new PrismaAttributeArgValue( - 'FieldReference', - new PrismaFieldReference( - resolved(node.target).name, - node.args.map((arg) => new PrismaFieldReferenceArg(arg.name, this.exprToText(arg.value))) - ) - ); - } else if (isInvocationExpr(node)) { - // invocation - return new PrismaAttributeArgValue('FunctionCall', this.makeFunctionCall(node)); - } else { - throw new PluginError(name, `Unsupported attribute argument expression type: ${node.$type}`); - } - } - - makeFunctionCall(node: InvocationExpr): PrismaFunctionCall { - return new PrismaFunctionCall( - resolved(node.function).name, - node.args.map((arg) => { - const val = match(arg.value) - .when(isStringLiteral, (v) => `"${v.value}"`) - .when(isLiteralExpr, (v) => v.value.toString()) - .when(isNullExpr, () => 'null') - .otherwise(() => { - throw new PluginError(name, 'Function call argument must be literal or null'); - }); - - return new PrismaFunctionCallArg(val); - }) - ); - } - - private generateContainerAttribute(container: PrismaContainerDeclaration, attr: DataModelAttribute) { - const attrName = resolved(attr.decl).name; - if (attrName === MODEL_PASSTHROUGH_ATTR) { - const text = getLiteral(attr.args[0].value); - if (text) { - container.attributes.push(new PrismaPassThroughAttribute(text)); - } - } else { - container.attributes.push( - new PrismaModelAttribute( - attrName, - attr.args.map((arg) => this.makeAttributeArg(arg)) - ) - ); - } - } - - private generateEnum(prisma: PrismaModel, decl: Enum) { - const _enum = prisma.addEnum(decl.name); - - for (const field of decl.fields) { - this.generateEnumField(_enum, field); - } - - for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) { - this.generateContainerAttribute(_enum, attr); - } - - // user defined comments pass-through - decl.comments.forEach((c) => _enum.addComment(c)); - this.getCustomAttributesAsComments(decl).forEach((c) => _enum.addComment(c)); - } - - private generateEnumField(_enum: PrismaEnum, field: EnumField) { - const attributes = field.attributes - .filter((attr) => this.isPrismaAttribute(attr)) - .map((attr) => this.makeFieldAttribute(attr)); - - const docs = [...field.comments, ...this.getCustomAttributesAsComments(field)]; - _enum.addField(field.name, attributes, docs); - } - - private getCustomAttributesAsComments(decl: DataModel | DataModelField | Enum | EnumField) { - if (!this.customAttributesAsComments) { - return []; - } else { - return decl.attributes - .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) - .map((attr) => `/// ${this.zModelGenerator.generate(attr)}`); - } - } -} - -function isDescendantOf(model: DataModel, superModel: DataModel): boolean { - return model.superTypes.some((s) => s.ref === superModel || isDescendantOf(s.ref!, superModel)); -} diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts deleted file mode 100644 index 398ee3995..000000000 --- a/packages/schema/src/plugins/zod/generator.ts +++ /dev/null @@ -1,824 +0,0 @@ -import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; -import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { - ExpressionContext, - PluginError, - PluginGlobalOptions, - PluginOptions, - RUNTIME_PACKAGE, - TypeScriptExpressionTransformer, - TypeScriptExpressionTransformerError, - ensureEmptyDir, - getAttributeArg, - getAttributeArgLiteral, - getDataModels, - getLiteralArray, - hasAttribute, - isDataModelFieldReference, - isDiscriminatorField, - isEnumFieldReference, - isForeignKeyField, - isFromStdlib, - isIdField, - parseOptionAsStrings, - resolvePath, - saveSourceFile, -} from '@zenstackhq/sdk'; -import { - DataModel, - DataModelField, - EnumField, - Model, - TypeDef, - isArrayExpr, - isDataModel, - isEnum, - isTypeDef, -} from '@zenstackhq/sdk/ast'; -import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; -import { getPrismaClientImportSpec, supportCreateMany, type DMMF } from '@zenstackhq/sdk/prisma'; -import { streamAllContents } from 'langium'; -import path from 'path'; -import type { CodeBlockWriter, SourceFile } from 'ts-morph'; -import { name } from '.'; -import { getDefaultOutputFolder } from '../plugin-utils'; -import Transformer from './transformer'; -import { ObjectMode } from './types'; -import { makeFieldSchema } from './utils/schema-gen'; - -export class ZodSchemaGenerator { - private readonly sourceFiles: SourceFile[] = []; - private readonly globalOptions: PluginGlobalOptions; - private readonly mode: ObjectMode; - private readonly zodVersion: 'v3' | 'v4' = 'v3'; - - constructor( - private readonly model: Model, - private readonly options: PluginOptions, - private readonly dmmf: DMMF.Document, - globalOptions: PluginGlobalOptions | undefined - ) { - if (!globalOptions) { - throw new Error('Global options are required'); - } - this.globalOptions = globalOptions; - - // options validation - if ( - this.options.mode && - (typeof this.options.mode !== 'string' || !['strip', 'strict', 'passthrough'].includes(this.options.mode)) - ) { - throw new PluginError( - name, - `Invalid mode option: "${this.options.mode}". Must be one of 'strip', 'strict', or 'passthrough'.` - ); - } - - this.mode = (this.options.mode ?? 'strict') as ObjectMode; - - if (this.options.version) { - if (typeof this.options.version !== 'string' || !['v3', 'v4'].includes(this.options.version)) { - throw new PluginError( - name, - `Invalid "version" option: "${this.options.version}". Must be one of 'v3' or 'v4'.` - ); - } - this.zodVersion = this.options.version as 'v3' | 'v4'; - } - } - - async generate() { - let output = this.options.output as string; - if (!output) { - const defaultOutputFolder = getDefaultOutputFolder(this.globalOptions); - if (defaultOutputFolder) { - output = path.join(defaultOutputFolder, 'zod'); - } else { - output = './generated/zod'; - } - } - output = resolvePath(output, this.options); - ensureEmptyDir(output); - Transformer.setOutputPath(output); - - // calculate the models to be excluded - const excludeModels = this.getExcludedModels(); - - const prismaClientDmmf = this.dmmf; - - const modelOperations = prismaClientDmmf.mappings.modelOperations.filter( - (o) => !excludeModels.find((e) => e === o.model) - ); - - const inputObjectTypes = (prismaClientDmmf.schema.inputObjectTypes.prisma ?? []).filter( - (type) => - !excludeModels.some((e) => type.name.toLowerCase().startsWith(e.toLocaleLowerCase())) && - // exclude delegate aux related types - !type.name.toLowerCase().includes(DELEGATE_AUX_RELATION_PREFIX) - ); - - const outputObjectTypes = prismaClientDmmf.schema.outputObjectTypes.prisma.filter( - (type) => - !excludeModels.some((e) => type.name.toLowerCase().startsWith(e.toLowerCase())) && - // exclude delegate aux related types - !type.name.toLowerCase().includes(DELEGATE_AUX_RELATION_PREFIX) - ); - - const models: DMMF.Model[] = prismaClientDmmf.datamodel.models.filter( - (m) => !excludeModels.find((e) => e === m.name) - ); - - // common schemas - await this.generateCommonSchemas(output); - - // enums - await this.generateEnumSchemas( - prismaClientDmmf.schema.enumTypes.prisma, - prismaClientDmmf.schema.enumTypes.model ?? [] - ); - - await this.generateModelSchemas(output, excludeModels); - - if (this.options.modelOnly) { - // generate stub for object and input schemas, so the exports from '@zenstackhq/runtime/zod' are available - this.sourceFiles.push( - this.project.createSourceFile(path.join(output, 'objects', 'index.ts'), '', { overwrite: true }) - ); - this.sourceFiles.push( - this.project.createSourceFile(path.join(output, 'input', 'index.ts'), '', { overwrite: true }) - ); - } else { - // detailed object schemas referenced from input schemas - addMissingInputObjectTypes(inputObjectTypes, outputObjectTypes, models); - const aggregateOperationSupport = resolveAggregateOperationSupport(inputObjectTypes); - await this.generateObjectSchemas(inputObjectTypes, output); - - // input schemas - const transformer = new Transformer({ - models, - modelOperations, - aggregateOperationSupport, - project: this.project, - inputObjectTypes, - zmodel: this.model, - mode: this.mode, - zodVersion: this.zodVersion, - }); - await transformer.generateInputSchemas(this.options, this.model); - this.sourceFiles.push(...transformer.sourceFiles); - } - - // create barrel file - const exports = [`export * as models from './models'`, `export * as enums from './enums'`]; - if (this.options.modelOnly !== true) { - exports.push(`export * as input from './input'`, `export * as objects from './objects'`); - } - this.sourceFiles.push( - this.project.createSourceFile(path.join(output, 'index.ts'), exports.join(';\n'), { overwrite: true }) - ); - - if (this.options.preserveTsFiles === true || this.options.output) { - // if preserveTsFiles is true or the user provided a custom output directory, - // save the generated files - this.sourceFiles.forEach(saveSourceFile); - } - } - - private get project() { - return this.globalOptions.tsProject; - } - - private getExcludedModels() { - // resolve "generateModels" option - const generateModels = parseOptionAsStrings(this.options, 'generateModels', name); - if (generateModels) { - if (this.options.modelOnly === true) { - // no model reference needs to be considered, directly exclude any model not included - return this.model.declarations - .filter((d) => isDataModel(d) && !generateModels.includes(d.name)) - .map((m) => m.name); - } else { - // calculate a transitive closure of models to be included - const todo = getDataModels(this.model).filter((dm) => generateModels.includes(dm.name)); - const included = new Set(); - while (todo.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const dm = todo.pop()!; - included.add(dm); - - // add referenced models to the todo list - dm.fields - .map((f) => f.type.reference?.ref) - .filter((type): type is DataModel => isDataModel(type)) - .forEach((type) => { - if (!included.has(type)) { - todo.push(type); - } - }); - } - - // finally find the models to be excluded - return getDataModels(this.model) - .filter((dm) => !included.has(dm)) - .map((m) => m.name); - } - } else { - return []; - } - } - - private async generateCommonSchemas(output: string) { - // Decimal - this.sourceFiles.push( - this.project.createSourceFile( - path.join(output, 'common', 'index.ts'), - ` - import { z } from 'zod/${this.zodVersion}'; - export const DecimalSchema = z.any().refine((val) => { - if (typeof val === 'string' || typeof val === 'number') { - return true; - } - // Decimal.js shape - if (typeof val === 'object' && val && Array.isArray(val.d) && typeof val.e === 'number' && typeof val.s === 'number') { - return true; - } - return false; - }, { message: 'Not a valid Decimal value'}); - `, - { overwrite: true } - ) - ); - } - - private async generateEnumSchemas( - prismaSchemaEnum: readonly DMMF.SchemaEnum[], - modelSchemaEnum: readonly DMMF.SchemaEnum[] - ) { - const enumTypes = [...prismaSchemaEnum, ...modelSchemaEnum]; - const enumNames = enumTypes.map((enumItem) => upperCaseFirst(enumItem.name)); - Transformer.enumNames = enumNames ?? []; - const transformer = new Transformer({ - enumTypes, - project: this.project, - inputObjectTypes: [], - zmodel: this.model, - mode: this.mode, - zodVersion: this.zodVersion, - }); - await transformer.generateEnumSchemas(); - this.sourceFiles.push(...transformer.sourceFiles); - } - - private async generateObjectSchemas(inputObjectTypes: DMMF.InputType[], output: string) { - // whether Prisma's Unchecked* series of input types should be generated - const generateUnchecked = this.options.noUncheckedInput !== true; - - const moduleNames: string[] = []; - for (let i = 0; i < inputObjectTypes.length; i += 1) { - // exclude delegate aux fields - const fields = inputObjectTypes[i]?.fields?.filter((f) => !f.name.includes(DELEGATE_AUX_RELATION_PREFIX)); - const name = inputObjectTypes[i]?.name; - - if (!generateUnchecked && name.includes('Unchecked')) { - continue; - } - - if (name.includes('CreateMany') && !supportCreateMany(this.model)) { - continue; - } - - const transformer = new Transformer({ - name, - fields, - project: this.project, - inputObjectTypes, - zmodel: this.model, - mode: this.mode, - zodVersion: this.zodVersion, - }); - const moduleName = transformer.generateObjectSchema(generateUnchecked, this.options); - moduleNames.push(moduleName); - this.sourceFiles.push(...transformer.sourceFiles); - } - - this.sourceFiles.push( - this.project.createSourceFile( - path.join(output, 'objects/index.ts'), - moduleNames.map((name) => `export * from './${name}';`).join('\n'), - { overwrite: true } - ) - ); - } - - private async generateModelSchemas(output: string, excludedModels: string[]) { - const schemaNames: string[] = []; - for (const dm of getDataModels(this.model)) { - if (!excludedModels.includes(dm.name)) { - schemaNames.push(await this.generateModelSchema(dm, output)); - } - } - - for (const typeDef of this.model.declarations.filter(isTypeDef)) { - if (!excludedModels.includes(typeDef.name)) { - schemaNames.push(await this.generateTypeDefSchema(typeDef, output)); - } - } - - this.sourceFiles.push( - this.project.createSourceFile( - path.join(output, 'models', 'index.ts'), - schemaNames.map((name) => `export * from './${name}';`).join('\n'), - { overwrite: true } - ) - ); - } - - private generateTypeDefSchema(typeDef: TypeDef, output: string) { - const schemaName = `${upperCaseFirst(typeDef.name)}.schema`; - const sf = this.project.createSourceFile(path.join(output, 'models', `${schemaName}.ts`), undefined, { - overwrite: true, - }); - this.sourceFiles.push(sf); - sf.replaceWithText((writer) => { - this.addPreludeAndImports(typeDef, writer); - - writer.write(`const baseSchema = z.object(`); - writer.inlineBlock(() => { - typeDef.fields.forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); - }); - }); - - switch (this.options.mode) { - case 'strip': - // zod strips by default - writer.writeLine(')'); - break; - case 'passthrough': - writer.writeLine(').passthrough();'); - break; - default: - writer.writeLine(').strict();'); - break; - } - - // compile "@@validate" to a function calling zod's `.refine()` - const refineFuncName = this.createRefineFunction(typeDef, writer); - - if (refineFuncName) { - // export a schema without refinement for extensibility: `[Model]WithoutRefineSchema` - const noRefineSchema = `${upperCaseFirst(typeDef.name)}WithoutRefineSchema`; - writer.writeLine(` -/** - * \`${typeDef.name}\` schema prior to calling \`.refine()\` for extensibility. - */ -export const ${noRefineSchema} = baseSchema; -export const ${typeDef.name}Schema = ${refineFuncName}(${noRefineSchema}); -`); - } else { - writer.writeLine(`export const ${typeDef.name}Schema = baseSchema;`); - } - }); - - return schemaName; - } - - private addPreludeAndImports(decl: DataModel | TypeDef, writer: CodeBlockWriter) { - writer.writeLine(`import { z } from 'zod/${this.zodVersion}';`); - - // import user-defined enums from Prisma as they might be referenced in the expressions - const importEnums = new Set(); - for (const node of streamAllContents(decl)) { - if (isEnumFieldReference(node)) { - const field = node.target.ref as EnumField; - if (!isFromStdlib(field.$container)) { - importEnums.add(field.$container.name); - } - } - } - - // import enum schemas - const importedEnumSchemas = new Set(); - for (const field of decl.fields) { - if (field.type.reference?.ref && isEnum(field.type.reference?.ref)) { - const name = upperCaseFirst(field.type.reference?.ref.name); - if (!importedEnumSchemas.has(name)) { - writer.writeLine(`import { ${name}Schema } from '../enums/${name}.schema';`); - importedEnumSchemas.add(name); - } - } - } - - // import Decimal - if (decl.fields.some((field) => field.type.type === 'Decimal')) { - writer.writeLine(`import { DecimalSchema } from '../common';`); - writer.writeLine(`import { Decimal } from 'decimal.js';`); - } - - // import referenced types' schemas - const referencedTypes = new Set( - decl.fields - .filter((field) => isTypeDef(field.type.reference?.ref) && field.type.reference?.ref.name !== decl.name) - .map((field) => field.type.reference!.ref!.name) - ); - for (const refType of referencedTypes) { - writer.writeLine(`import { ${upperCaseFirst(refType)}Schema } from './${upperCaseFirst(refType)}.schema';`); - } - } - - private async generateModelSchema(model: DataModel, output: string) { - const schemaName = `${upperCaseFirst(model.name)}.schema`; - const sf = this.project.createSourceFile(path.join(output, 'models', `${schemaName}.ts`), undefined, { - overwrite: true, - }); - this.sourceFiles.push(sf); - sf.replaceWithText((writer) => { - const scalarFields = model.fields.filter( - (field) => - // id fields are always included - isIdField(field) || - // regular fields only - (!isDataModel(field.type.reference?.ref) && !isForeignKeyField(field)) - ); - - const relations = model.fields.filter((field) => isDataModel(field.type.reference?.ref)); - const fkFields = model.fields.filter((field) => isForeignKeyField(field)); - - this.addPreludeAndImports(model, writer); - - // base schema - including all scalar fields, with optionality following the schema - this.createModelBaseSchema('baseSchema', writer, scalarFields, true); - - // base schema without field defaults - this.createModelBaseSchema('baseSchemaWithoutDefaults', writer, scalarFields, false); - - // relation fields - - let relationSchema: string | undefined; - let fkSchema: string | undefined; - - if (relations.length > 0) { - relationSchema = 'relationSchema'; - writer.write(`const ${relationSchema} = z.object(`); - writer.inlineBlock(() => { - [...relations].forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); - }); - }); - writer.writeLine(');'); - } - - if (fkFields.length > 0) { - fkSchema = 'fkSchema'; - writer.write(`const ${fkSchema} = z.object(`); - writer.inlineBlock(() => { - fkFields.forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field)},`); - }); - }); - writer.writeLine(');'); - } - - // compile "@@validate" to ".refine" - const refineFuncName = this.createRefineFunction(model, writer); - - // delegate discriminator fields are to be excluded from mutation schemas - const delegateDiscriminatorFields = model.fields.filter((field) => isDiscriminatorField(field)); - const omitDiscriminators = - delegateDiscriminatorFields.length > 0 - ? `.omit({ ${delegateDiscriminatorFields.map((f) => `${f.name}: true`).join(', ')} })` - : ''; - - //////////////////////////////////////////////// - // 1. Model schema - //////////////////////////////////////////////// - - let modelSchema = 'baseSchema'; - - // omit fields - const fieldsToOmit = scalarFields.filter((field) => hasAttribute(field, '@omit')); - if (fieldsToOmit.length > 0) { - modelSchema = this.makeOmit( - modelSchema, - fieldsToOmit.map((f) => f.name) - ); - } - - // export schema with only scalar fields: `[Model]ScalarSchema` - const modelScalarSchema = `${upperCaseFirst(model.name)}ScalarSchema`; - writer.writeLine(` -/** - * \`${model.name}\` schema excluding foreign keys and relations. - */ -export const ${modelScalarSchema} = ${modelSchema}; -`); - modelSchema = modelScalarSchema; - - // merge fk fields - if (fkSchema) { - modelSchema = this.makeMerge(modelSchema, fkSchema); - } - - // merge relation fields (all optional) - if (relationSchema) { - modelSchema = this.makeMerge(modelSchema, this.makePartial(relationSchema)); - } - - // refine - if (refineFuncName) { - // export a schema without refinement for extensibility: `[Model]WithoutRefineSchema` - const noRefineSchema = `${upperCaseFirst(model.name)}WithoutRefineSchema`; - writer.writeLine(` -/** - * \`${model.name}\` schema prior to calling \`.refine()\` for extensibility. - */ -export const ${noRefineSchema} = ${modelSchema}; -`); - modelSchema = `${refineFuncName}(${noRefineSchema})`; - } - - // export the final model schema: `[Model]Schema` - writer.writeLine(` -/** - * \`${model.name}\` schema including all fields (scalar, foreign key, and relations) and validations. - */ -export const ${upperCaseFirst(model.name)}Schema = ${modelSchema}; -`); - - //////////////////////////////////////////////// - // 2. Prisma create & update - //////////////////////////////////////////////// - - // schema for validating prisma create input (all fields optional) - let prismaCreateSchema = this.makePassthrough( - this.makePartial(`baseSchemaWithoutDefaults${omitDiscriminators}`) - ); - if (refineFuncName) { - prismaCreateSchema = `${refineFuncName}(${prismaCreateSchema})`; - } - writer.writeLine(` -/** - * Schema used for validating Prisma create input. For internal use only. - * @private - */ -export const ${upperCaseFirst(model.name)}PrismaCreateSchema = ${prismaCreateSchema}; -`); - - // schema for validating prisma update input (all fields optional) - // note numeric fields can be simple update or atomic operations - let prismaUpdateSchema = `z.object({ - ${scalarFields - .filter((f) => !isDiscriminatorField(f)) - .map((field) => { - let fieldSchema = makeFieldSchema(field, false); - if (field.type.type === 'Int' || field.type.type === 'Float') { - fieldSchema = `z.union([${fieldSchema}, z.record(z.string(), z.unknown())])`; - } - return `\t${field.name}: ${fieldSchema}`; - }) - .join(',\n')} - })`; - prismaUpdateSchema = this.makePassthrough(this.makePartial(prismaUpdateSchema)); - writer.writeLine( - ` -/** - * Schema used for validating Prisma update input. For internal use only. - * @private - */ -export const ${upperCaseFirst(model.name)}PrismaUpdateSchema = ${prismaUpdateSchema}; -` - ); - - //////////////////////////////////////////////// - // 3. Create schema - //////////////////////////////////////////////// - - let createSchema = `baseSchemaWithoutDefaults${omitDiscriminators}`; - const fieldsWithDefault = scalarFields.filter( - (field) => hasAttribute(field, '@default') || hasAttribute(field, '@updatedAt') || field.type.array - ); - - // mark fields with default as optional - if (fieldsWithDefault.length > 0) { - // delegate discriminator fields are omitted from the base schema, so we need - // to take care not to make them partial otherwise the schema won't compile - createSchema = this.makePartial( - createSchema, - fieldsWithDefault.filter((f) => !delegateDiscriminatorFields.includes(f)).map((f) => f.name) - ); - } - - // export schema with only scalar fields: `[Model]CreateScalarSchema` - const createScalarSchema = `${upperCaseFirst(model.name)}CreateScalarSchema`; - writer.writeLine(` -/** - * \`${model.name}\` schema for create operations excluding foreign keys and relations. - */ -export const ${createScalarSchema} = ${createSchema}; -`); - - if (fkSchema) { - // merge fk fields - createSchema = this.makeMerge(createScalarSchema, fkSchema); - } - - if (refineFuncName) { - // export a schema without refinement for extensibility: `[Model]CreateWithoutRefineSchema` - const noRefineSchema = `${upperCaseFirst(model.name)}CreateWithoutRefineSchema`; - writer.writeLine(` -/** - * \`${model.name}\` schema for create operations prior to calling \`.refine()\` for extensibility. - */ -export const ${noRefineSchema} = ${createSchema}; -`); - createSchema = `${refineFuncName}(${noRefineSchema})`; - } - - // export the final create schema: `[Model]CreateSchema` - writer.writeLine(` -/** - * \`${model.name}\` schema for create operations including scalar fields, foreign key fields, and validations. - */ -export const ${upperCaseFirst(model.name)}CreateSchema = ${createSchema}; -`); - - //////////////////////////////////////////////// - // 3. Update schema - //////////////////////////////////////////////// - - // for update all fields are optional - let updateSchema = this.makePartial(`baseSchemaWithoutDefaults${omitDiscriminators}`); - - // export schema with only scalar fields: `[Model]UpdateScalarSchema` - const updateScalarSchema = `${upperCaseFirst(model.name)}UpdateScalarSchema`; - writer.writeLine(` -/** - * \`${model.name}\` schema for update operations excluding foreign keys and relations. - */ -export const ${updateScalarSchema} = ${updateSchema}; -`); - - updateSchema = updateScalarSchema; - - if (fkSchema) { - // merge fk fields - updateSchema = this.makeMerge(updateSchema, this.makePartial(fkSchema)); - } - - if (refineFuncName) { - // export a schema without refinement for extensibility: `[Model]UpdateWithoutRefineSchema` - const noRefineSchema = `${upperCaseFirst(model.name)}UpdateWithoutRefineSchema`; - writer.writeLine(` -/** - * \`${model.name}\` schema for update operations prior to calling \`.refine()\` for extensibility. - */ -export const ${noRefineSchema} = ${updateSchema}; -`); - updateSchema = `${refineFuncName}(${noRefineSchema})`; - } - - // export the final update schema: `[Model]UpdateSchema` - writer.writeLine(` -/** - * \`${model.name}\` schema for update operations including scalar fields, foreign key fields, and validations. - */ -export const ${upperCaseFirst(model.name)}UpdateSchema = ${updateSchema}; -`); - }); - - return schemaName; - } - - private createModelBaseSchema( - name: string, - writer: CodeBlockWriter, - scalarFields: DataModelField[], - addDefaults: boolean - ) { - writer.write(`const ${name} = z.object(`); - writer.inlineBlock(() => { - scalarFields.forEach((field) => { - writer.writeLine(`${field.name}: ${makeFieldSchema(field, addDefaults)},`); - }); - }); - - switch (this.options.mode) { - case 'strip': - // zod strips by default - writer.writeLine(')'); - break; - case 'passthrough': - writer.writeLine(').passthrough();'); - break; - default: - writer.writeLine(').strict();'); - break; - } - } - - private createRefineFunction(decl: DataModel | TypeDef, writer: CodeBlockWriter) { - const refinements = this.makeValidationRefinements(decl); - let refineFuncName: string | undefined; - if (refinements.length > 0) { - refineFuncName = `refine${upperCaseFirst(decl.name)}`; - writer.writeLine( - ` - /** - * Schema refinement function for applying \`@@validate\` rules. - */ - export function ${refineFuncName}(schema: z.ZodType) { return schema${refinements.join('\n')}; - } - ` - ); - return refineFuncName; - } else { - return undefined; - } - } - - private makeValidationRefinements(decl: DataModel | TypeDef) { - const attrs = decl.attributes.filter((attr) => attr.decl.ref?.name === '@@validate'); - const refinements = attrs - .map((attr) => { - const valueArg = getAttributeArg(attr, 'value'); - if (!valueArg) { - return undefined; - } - - const messageArg = getAttributeArgLiteral(attr, 'message'); - const message = messageArg ? `message: ${JSON.stringify(messageArg)},` : ''; - - const pathArg = getAttributeArg(attr, 'path'); - const path = - pathArg && isArrayExpr(pathArg) - ? `path: ['${getLiteralArray(pathArg)?.join(`', '`)}'],` - : ''; - - const options = `, { ${message} ${path} }`; - - try { - let expr = new TypeScriptExpressionTransformer({ - context: ExpressionContext.ValidationRule, - fieldReferenceContext: 'value', - useLiteralEnum: true, - }).transform(valueArg); - - if (isDataModelFieldReference(valueArg)) { - // if the expression is a simple field reference, treat undefined - // as true since the all fields are optional in validation context - expr = `${expr} ?? true`; - } - - return `.refine((value: any) => ${expr}${options})`; - } catch (err) { - if (err instanceof TypeScriptExpressionTransformerError) { - throw new PluginError(name, err.message); - } else { - throw err; - } - } - }) - .filter((r) => !!r); - - return refinements; - } - - private makePartial(schema: string, fields?: string[]) { - if (fields) { - if (fields.length === 0) { - return schema; - } else { - return `${schema}.partial({ - ${fields.map((f) => `${f}: true`).join(', ')} - })`; - } - } else { - return `${schema}.partial()`; - } - } - - private makeOmit(schema: string, fields: string[]) { - return `${schema}.omit({ - ${fields.map((f) => `${f}: true`).join(', ')}, - })`; - } - - private makeMerge(schema1: string, schema2: string): string { - return `${schema1}.merge(${schema2})`; - } - - private makePassthrough(schema: string) { - return `${schema}.passthrough()`; - } -} - -export function computePrismaClientImport(importingFrom: string, options: PluginOptions) { - let importPath = getPrismaClientImportSpec(importingFrom, options); - if (importPath.startsWith(RUNTIME_PACKAGE) && !options.output) { - // default import from `@zenstackhq/runtime` and this plugin is generating - // into default location, we should correct the prisma client import into a - // importing from `.zenstack` to avoid cyclic dependencies with runtime - importPath = importPath.replace(RUNTIME_PACKAGE, '.zenstack'); - } - return importPath; -} diff --git a/packages/schema/src/plugins/zod/index.ts b/packages/schema/src/plugins/zod/index.ts deleted file mode 100644 index 074002ee0..000000000 --- a/packages/schema/src/plugins/zod/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PluginFunction } from '@zenstackhq/sdk'; -import { invariant } from '@zenstackhq/runtime/local-helpers'; -import { ZodSchemaGenerator } from './generator'; - -export const name = 'Zod'; -export const description = 'Generating Zod schemas'; - -const run: PluginFunction = async (model, options, dmmf, globalOptions) => { - invariant(dmmf); - const generator = new ZodSchemaGenerator(model, options, dmmf, globalOptions); - return generator.generate(); -}; - -export default run; diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts deleted file mode 100644 index 45bd5f06a..000000000 --- a/packages/schema/src/plugins/zod/transformer.ts +++ /dev/null @@ -1,960 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime'; -import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { - getForeignKeyFields, - getRelationBackLink, - hasAttribute, - indentString, - isDelegateModel, - isDiscriminatorField, - type PluginOptions, -} from '@zenstackhq/sdk'; -import { DataModel, DataModelField, Enum, isDataModel, isEnum, isTypeDef, type Model } from '@zenstackhq/sdk/ast'; -import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; -import { supportCreateMany, type DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; -import path from 'path'; -import type { Project, SourceFile } from 'ts-morph'; -import { computePrismaClientImport } from './generator'; -import { AggregateOperationSupport, ObjectMode, TransformerParams } from './types'; - -export default class Transformer { - name: string; - originalName: string; - fields: readonly PrismaDMMF.SchemaArg[]; - schemaImports = new Set(); - models: readonly PrismaDMMF.Model[]; - modelOperations: PrismaDMMF.ModelMapping[]; - aggregateOperationSupport: AggregateOperationSupport; - enumTypes: readonly PrismaDMMF.SchemaEnum[]; - - static enumNames: string[] = []; - static rawOpsMap: { [name: string]: string } = {}; - private static outputPath = './generated'; - private hasJson = false; - private hasDecimal = false; - private project: Project; - private inputObjectTypes: PrismaDMMF.InputType[]; - public sourceFiles: SourceFile[] = []; - private zmodel: Model; - private mode: ObjectMode; - private zodVersion: 'v3' | 'v4'; - - constructor(params: TransformerParams) { - this.originalName = params.name ?? ''; - this.name = params.name ? upperCaseFirst(params.name) : ''; - this.fields = params.fields ?? []; - this.models = params.models ?? []; - this.modelOperations = params.modelOperations ?? []; - this.aggregateOperationSupport = params.aggregateOperationSupport ?? {}; - this.enumTypes = params.enumTypes ?? []; - this.project = params.project; - this.inputObjectTypes = params.inputObjectTypes; - this.zmodel = params.zmodel; - this.mode = params.mode; - this.zodVersion = params.zodVersion; - } - - static setOutputPath(outPath: string) { - this.outputPath = outPath; - } - - static getOutputPath() { - return this.outputPath; - } - - async generateEnumSchemas() { - const generated: string[] = []; - - // generate for enums in DMMF - for (const enumType of this.enumTypes) { - const name = upperCaseFirst(enumType.name); - const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`); - const content = `${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( - `${name}`, - `z.enum(${JSON.stringify( - enumType.values - // exclude fields generated for delegate models - .filter((v) => !v.startsWith(DELEGATE_AUX_RELATION_PREFIX)) - )})` - )}`; - this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); - generated.push(enumType.name); - } - - // enums not referenced by data models are not in DMMF, deal with them separately - const extraEnums = this.zmodel.declarations.filter((d): d is Enum => isEnum(d) && !generated.includes(d.name)); - for (const enumDecl of extraEnums) { - const name = upperCaseFirst(enumDecl.name); - const filePath = path.join(Transformer.outputPath, `enums/${name}.schema.ts`); - const content = `${this.generateImportZodStatement()}\n${this.generateExportSchemaStatement( - `${name}`, - `z.enum(${JSON.stringify(enumDecl.fields.map((f) => f.name))})` - )}`; - this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); - generated.push(enumDecl.name); - } - - this.sourceFiles.push( - this.project.createSourceFile( - path.join(Transformer.outputPath, `enums/index.ts`), - generated.map((name) => `export * from './${upperCaseFirst(name)}.schema';`).join('\n'), - { overwrite: true } - ) - ); - } - - generateImportZodStatement() { - let r = `import { z } from 'zod/${this.zodVersion}';\n`; - if (this.mode === 'strip') { - // import the additional `smartUnion` helper - r += `import { smartUnion } from '@zenstackhq/runtime/zod-utils';\n`; - } - return r; - } - - generateExportSchemaStatement(name: string, schema: string) { - return `export const ${name}Schema = ${schema}`; - } - - generateObjectSchema(generateUnchecked: boolean, options: PluginOptions) { - const { schemaFields, extraImports } = this.generateObjectSchemaFields(generateUnchecked); - const objectSchema = this.prepareObjectSchema(schemaFields, options); - - const filePath = path.join(Transformer.outputPath, `objects/${this.name}.schema.ts`); - const content = extraImports.join('\n\n') + objectSchema; - this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); - return `${this.name}.schema`; - } - - private createUpdateInputRegex = /(\S+?)(Unchecked)?(Create|Update|CreateMany|UpdateMany).*Input/; - - generateObjectSchemaFields(generateUnchecked: boolean) { - let fields = this.fields; - let contextDataModel: DataModel | undefined; - const extraImports: string[] = []; - - // exclude discriminator fields from create/update input schemas - const createUpdateMatch = this.createUpdateInputRegex.exec(this.name); - if (createUpdateMatch) { - const modelName = createUpdateMatch[1]; - contextDataModel = this.zmodel.declarations.find( - (d): d is DataModel => isDataModel(d) && d.name === modelName - ); - - if (contextDataModel) { - // exclude discriminator fields from create/update input schemas - const discriminatorFields = contextDataModel.fields.filter(isDiscriminatorField); - if (discriminatorFields.length > 0) { - fields = fields.filter((field) => { - return !discriminatorFields.some( - (discriminatorField) => discriminatorField.name === field.name - ); - }); - } - - // import type-def's schemas - const typeDefFields = contextDataModel.fields.filter((f) => isTypeDef(f.type.reference?.ref)); - typeDefFields.forEach((field) => { - const typeName = upperCaseFirst(field.type.reference!.$refText); - const importLine = `import { ${typeName}Schema } from '../models/${typeName}.schema';`; - if (!extraImports.includes(importLine)) { - extraImports.push(importLine); - } - }); - } - } - - const zodObjectSchemaFields = fields - .map((field) => - this.generateObjectSchemaField(field, contextDataModel, generateUnchecked, !!createUpdateMatch) - ) - .flatMap((item) => item) - .map((item) => { - const [zodStringWithMainType, field, skipValidators] = item; - - const value = skipValidators - ? zodStringWithMainType - : this.generateFieldValidators(zodStringWithMainType, field); - - return value.trim(); - }); - return { schemaFields: zodObjectSchemaFields, extraImports }; - } - - generateObjectSchemaField( - field: PrismaDMMF.SchemaArg, - contextDataModel: DataModel | undefined, - generateUnchecked: boolean, - replaceJsonWithTypeDef = false - ): [string, PrismaDMMF.SchemaArg, boolean][] { - const lines = field.inputTypes; - - if (lines.length === 0) { - return []; - } - - let alternatives: string[] | undefined = undefined; - - if (replaceJsonWithTypeDef) { - const dmField = contextDataModel?.fields.find((f) => f.name === field.name); - if (isTypeDef(dmField?.type.reference?.ref)) { - const isList = dmField.type.array; - const lazyStr = `z.lazy(() => ${upperCaseFirst(dmField.type.reference!.$refText)}Schema)`; - alternatives = [isList ? `${lazyStr}.array()` : lazyStr]; - } - } - - if (!alternatives) { - alternatives = lines.reduce((result, inputType) => { - if (!generateUnchecked && typeof inputType.type === 'string' && inputType.type.includes('Unchecked')) { - return result; - } - - if (inputType.type.includes('CreateMany') && !supportCreateMany(this.zmodel)) { - return result; - } - - // TODO: unify the following with `schema-gen.ts` - - if (inputType.type === 'String') { - result.push(this.wrapWithZodValidators('z.string()', field, inputType)); - } else if (inputType.type === 'Int' || inputType.type === 'Float') { - result.push(this.wrapWithZodValidators('z.number()', field, inputType)); - } else if (inputType.type === 'Decimal') { - this.hasDecimal = true; - result.push(this.wrapWithZodValidators('DecimalSchema', field, inputType)); - } else if (inputType.type === 'BigInt') { - result.push(this.wrapWithZodValidators('z.bigint()', field, inputType)); - } else if (inputType.type === 'Boolean') { - result.push(this.wrapWithZodValidators('z.boolean()', field, inputType)); - } else if (inputType.type === 'DateTime') { - result.push(this.wrapWithZodValidators(['z.date()', 'z.string().datetime()'], field, inputType)); - } else if (inputType.type === 'Bytes') { - result.push( - this.wrapWithZodValidators( - `z.custom(data => data instanceof Uint8Array)`, - field, - inputType - ) - ); - } else if (inputType.type === 'Json') { - this.hasJson = true; - result.push(this.wrapWithZodValidators('jsonSchema', field, inputType)); - } else if (inputType.type === 'True') { - result.push(this.wrapWithZodValidators('z.literal(true)', field, inputType)); - } else if (inputType.type === 'Null') { - result.push(this.wrapWithZodValidators('z.null()', field, inputType)); - } else { - const isEnum = inputType.location === 'enumTypes'; - const isFieldRef = inputType.location === 'fieldRefTypes'; - - if ( - // fieldRefTypes refer to other fields in the model and don't need to be generated as part of schema - !isFieldRef && - (inputType.namespace === 'prisma' || isEnum) - ) { - // reduce concrete input types to their delegate base types - // e.g.: "UserCreateNestedOneWithoutDelegate_aux_PostInput" => "UserCreateWithoutAssetInput" - let mappedInputType = inputType; - if (contextDataModel) { - mappedInputType = this.mapDelegateInputType(inputType, contextDataModel, field.name); - } - - if (mappedInputType.type !== this.originalName && typeof mappedInputType.type === 'string') { - this.addSchemaImport(mappedInputType.type); - } - - const contextField = contextDataModel?.fields.find((f) => f.name === field.name); - result.push(this.generatePrismaStringLine(field, mappedInputType, lines.length, contextField)); - } - } - - return result; - }, []); - } - - if (alternatives.length === 0) { - return []; - } - - if (alternatives.length > 1) { - alternatives = alternatives.map((alter) => alter.replace('.optional()', '')); - } - - const fieldName = alternatives.some((alt) => alt.includes(':')) ? '' : ` ${field.name}:`; - - let resString: string; - - if (alternatives.length === 1) { - resString = alternatives[0]; - } else { - if (alternatives.some((alt) => alt.includes('Unchecked'))) { - // if the union is for combining checked and unchecked input types, use `smartUnion` - // to parse with the best candidate at runtime - resString = this.wrapWithSmartUnion(...alternatives); - } else { - resString = `z.union([${alternatives.join(',\r\n')}])`; - } - } - - if (!field.isRequired) { - resString += '.optional()'; - } - - if (field.isNullable) { - resString += '.nullable()'; - } - - return [[` ${fieldName} ${resString} `, field, true]]; - } - - private mapDelegateInputType( - inputType: PrismaDMMF.InputTypeRef, - contextDataModel: DataModel, - contextFieldName: string - ) { - // input type mapping is only relevant for relation inherited from delegate models - const contextField = contextDataModel.fields.find((f) => f.name === contextFieldName); - if (!contextField || !isDataModel(contextField.type.reference?.ref)) { - return inputType; - } - - if (!contextField.$inheritedFrom || !isDelegateModel(contextField.$inheritedFrom)) { - return inputType; - } - - let processedInputType = inputType; - - // captures: model name and operation, "Without" part that references a concrete model, - // and the "Input" or "NestedInput" suffix - const match = inputType.type.match(/^(\S+?)((NestedOne)?WithoutDelegate_aux\S+?)((Nested)?Input)$/); - if (match) { - let mappedInputTypeName = match[1]; - - if (contextDataModel) { - // get the opposite side of the relation field, which should be of the proper - // delegate base type - const oppositeRelationField = getRelationBackLink(contextField); - if (oppositeRelationField) { - mappedInputTypeName += `Without${upperCaseFirst(oppositeRelationField.name)}`; - } - } - - // "Input" or "NestedInput" suffix - mappedInputTypeName += match[4]; - - // Prisma's naming is inconsistent for update input types, so we need - // to check for a few other candidates and use the one that matches - // a DMMF input type name - const candidates = [mappedInputTypeName]; - if (mappedInputTypeName.includes('UpdateOne')) { - candidates.push(...candidates.map((name) => name.replace('UpdateOne', 'Update'))); - } - if (mappedInputTypeName.includes('NestedInput')) { - candidates.push(...candidates.map((name) => name.replace('NestedInput', 'Input'))); - } - - const finalMappedName = - candidates.find((name) => this.inputObjectTypes.some((it) => it.name === name)) ?? mappedInputTypeName; - - processedInputType = { ...inputType, type: finalMappedName }; - } - return processedInputType; - } - - wrapWithZodValidators( - mainValidators: string | string[], - field: PrismaDMMF.SchemaArg, - inputType: PrismaDMMF.InputTypeRef - ) { - let line = ''; - - const base = Array.isArray(mainValidators) ? mainValidators : [mainValidators]; - - line += base - .map((validator) => { - let r = validator; - if (inputType.isList) { - r += '.array()'; - } - if (!field.isRequired) { - r += '.optional()'; - } - return r; - }) - .join(', '); - - if (base.length > 1) { - line = `z.union([${line}])`; - } - - return line; - } - - addSchemaImport(name: string) { - this.schemaImports.add(upperCaseFirst(name)); - } - - generatePrismaStringLine( - field: PrismaDMMF.SchemaArg, - inputType: PrismaDMMF.InputTypeRef, - inputsLength: number, - contextField: DataModelField | undefined - ) { - const isEnum = inputType.location === 'enumTypes'; - - const { isModelQueryType, modelName, queryName } = this.checkIsModelQueryType(inputType.type as string); - - const objectSchemaLine = isModelQueryType - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.resolveModelQuerySchemaName(modelName!, queryName!) - : `${upperCaseFirst(inputType.type.toString())}ObjectSchema`; - const enumSchemaLine = `${upperCaseFirst(inputType.type.toString())}Schema`; - - const schema = inputType.type === this.name ? objectSchemaLine : isEnum ? enumSchemaLine : objectSchemaLine; - - const arr = inputType.isList ? '.array()' : ''; - - const optional = - !field.isRequired || - // also check if the zmodel field infers the field as optional - (contextField && this.isFieldOptional(contextField)); - - return inputsLength === 1 - ? ` ${field.name}: z.lazy(() => ${schema})${arr}${optional ? '.optional()' : ''}` - : `z.lazy(() => ${schema})${arr}${optional ? '.optional()' : ''}`; - } - - private isFieldOptional(dmField: DataModelField) { - if (hasAttribute(dmField, '@default')) { - // it's possible that ZModel field has a default but it's transformed away - // when generating Prisma schema, e.g.: `@default(auth().id)` - return true; - } - - if (isDataModel(dmField.type.reference?.ref)) { - // if field is a relation, we need to check if the corresponding fk field has a default - // { - // authorId Int @default(auth().id) - // author User @relation(...) // <- author should be optional - // } - const fkFields = getForeignKeyFields(dmField); - if (fkFields.every((fkField) => hasAttribute(fkField, '@default'))) { - return true; - } - } - - return false; - } - - generateFieldValidators(zodStringWithMainType: string, field: PrismaDMMF.SchemaArg) { - const { isRequired, isNullable } = field; - - if (!isRequired) { - zodStringWithMainType += '.optional()'; - } - - if (isNullable) { - zodStringWithMainType += '.nullable()'; - } - - return zodStringWithMainType; - } - - prepareObjectSchema(zodObjectSchemaFields: string[], options: PluginOptions) { - const objectSchema = `${this.generateExportObjectSchemaStatement( - this.wrapWithZodObject(zodObjectSchemaFields, options.mode as string) - )}\n`; - - const prismaImportStatement = this.generateImportPrismaStatement(options); - - const json = this.generateJsonSchemaImplementation(); - - return `${this.generateObjectSchemaImportStatements()}${prismaImportStatement}${json}${objectSchema}`; - } - - generateExportObjectSchemaStatement(schema: string) { - let name = this.name; - let origName = this.originalName; - - if (isAggregateInputType(name)) { - name = `${name}Type`; - origName = `${origName}Type`; - } - const outType = this.makeZodType(`Prisma.${origName}`); - return `type SchemaType = ${outType}; -export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; - } - - generateImportPrismaStatement(options: PluginOptions) { - const prismaClientImportPath = computePrismaClientImport( - path.resolve(Transformer.outputPath, './objects'), - options - ); - return `import type { Prisma } from '${prismaClientImportPath}';\n\n`; - } - - generateJsonSchemaImplementation() { - let jsonSchemaImplementation = ''; - - if (this.hasJson) { - jsonSchemaImplementation += `\n`; - jsonSchemaImplementation += `const literalSchema = z.union([z.string(), z.number(), z.boolean()]);\n`; - jsonSchemaImplementation += `const jsonSchema: ${this.makeZodType('Prisma.InputJsonValue')} = z.lazy(() =>\n`; - jsonSchemaImplementation += ` z.union([literalSchema, z.array(jsonSchema.nullable()), z.record(z.string(), jsonSchema.nullable())])\n`; - jsonSchemaImplementation += `);\n\n`; - } - - return jsonSchemaImplementation; - } - - generateObjectSchemaImportStatements() { - let generatedImports = this.generateImportZodStatement(); - generatedImports += this.generateSchemaImports(); - generatedImports += this.generateCommonImport(); - generatedImports += '\n\n'; - return generatedImports; - } - - generateSchemaImports() { - return [...this.schemaImports] - .map((name) => { - const { isModelQueryType, modelName } = this.checkIsModelQueryType(name); - if (isModelQueryType) { - return `import { ${modelName}InputSchema } from '../input/${modelName}Input.schema';`; - } else if (Transformer.enumNames.includes(name)) { - return `import { ${name}Schema } from '../enums/${name}.schema';`; - } else { - return `import { ${name}ObjectSchema } from './${name}.schema';`; - } - }) - .join('\n'); - } - - private generateCommonImport() { - let r = ''; - if (this.hasDecimal) { - r += `import { DecimalSchema } from '../common';\n`; - } - if (r) { - r += '\n'; - } - return r; - } - - checkIsModelQueryType(type: string) { - const modelQueryTypeSuffixToQueryName: Record = { - FindManyArgs: 'findMany', - }; - for (const modelQueryType of ['FindManyArgs']) { - if (type.includes(modelQueryType)) { - const modelQueryTypeSuffixIndex = type.indexOf(modelQueryType); - return { - isModelQueryType: true, - modelName: upperCaseFirst(type.substring(0, modelQueryTypeSuffixIndex)), - queryName: modelQueryTypeSuffixToQueryName[modelQueryType], - }; - } - } - return { isModelQueryType: false }; - } - - resolveModelQuerySchemaName(modelName: string, queryName: string) { - return `${modelName}InputSchema.${queryName}`; - } - - wrapWithZodObject(zodStringFields: string | string[], mode = 'strict') { - let wrapped = ''; - - wrapped += 'z.object({'; - wrapped += '\n'; - wrapped += ' ' + zodStringFields; - wrapped += '\n'; - wrapped += '})'; - - switch (mode) { - case 'strip': - // zod strips by default - break; - case 'passthrough': - wrapped += '.passthrough()'; - break; - default: - wrapped += '.strict()'; - break; - } - return wrapped; - } - - wrapWithSmartUnion(...schemas: string[]) { - if (this.mode === 'strip') { - return `smartUnion(z, [${schemas.join(', ')}])`; - } else { - return `z.union([${schemas.join(', ')}])`; - } - } - - async generateInputSchemas(options: PluginOptions, zmodel: Model) { - const globalExports: string[] = []; - - // whether Prisma's Unchecked* series of input types should be generated - const generateUnchecked = options.noUncheckedInput !== true; - - for (const modelOperation of this.modelOperations) { - const { - model: origModelName, - findUnique, - findFirst, - findMany, - // @ts-expect-error - createOne, - createMany, - // @ts-expect-error - deleteOne, - // @ts-expect-error - updateOne, - deleteMany, - updateMany, - // @ts-expect-error - upsertOne, - aggregate, - groupBy, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } = modelOperation; - - const modelName = upperCaseFirst(origModelName); - - globalExports.push(`export * from './${modelName}Input.schema'`); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const model = findModelByName(this.models, origModelName)!; - - const { selectImport, includeImport, selectZodSchemaLineLazy, includeZodSchemaLineLazy } = - this.resolveSelectIncludeImportAndZodSchemaLine(model); - - let imports = [ - this.generateImportZodStatement(), - this.generateImportPrismaStatement(options), - selectImport, - includeImport, - ]; - let codeBody = ''; - const operations: [string, string][] = []; - const mode = (options.mode as string) ?? 'strict'; - - // OrderByWithRelationInput's name is different when "fullTextSearch" is enabled - const orderByWithRelationInput = this.inputObjectTypes - .map((o) => upperCaseFirst(o.name)) - .includes(`${modelName}OrderByWithRelationInput`) - ? `${modelName}OrderByWithRelationInput` - : `${modelName}OrderByWithRelationAndSearchRelevanceInput`; - - if (findUnique) { - imports.push( - `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'` - ); - const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereUniqueInputObjectSchema`; - codeBody += `findUnique: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['findUnique', origModelName]); - } - - if (findFirst) { - imports.push( - `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, - `import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`, - `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`, - `import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'` - ); - const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional()`; - codeBody += `findFirst: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['findFirst', origModelName]); - } - - if (findMany) { - imports.push( - `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, - `import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`, - `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`, - `import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'` - ); - const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional()`; - codeBody += `findMany: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['findMany', origModelName]); - } - - if (createOne) { - imports.push( - `import { ${modelName}CreateInputObjectSchema } from '../objects/${modelName}CreateInput.schema'` - ); - if (generateUnchecked) { - imports.push( - `import { ${modelName}UncheckedCreateInputObjectSchema } from '../objects/${modelName}UncheckedCreateInput.schema'` - ); - } - const dataSchema = generateUnchecked - ? this.wrapWithSmartUnion( - `${modelName}CreateInputObjectSchema`, - `${modelName}UncheckedCreateInputObjectSchema` - ) - : `${modelName}CreateInputObjectSchema`; - const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} data: ${dataSchema}`; - codeBody += `create: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['create', origModelName]); - } - - if (createMany && supportCreateMany(zmodel)) { - imports.push( - `import { ${modelName}CreateManyInputObjectSchema } from '../objects/${modelName}CreateManyInput.schema'` - ); - const fields = `data: z.union([${modelName}CreateManyInputObjectSchema, z.array(${modelName}CreateManyInputObjectSchema)]), skipDuplicates: z.boolean().optional()`; - codeBody += `createMany: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['createMany', origModelName]); - } - - if (deleteOne) { - imports.push( - `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'` - ); - const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereUniqueInputObjectSchema`; - codeBody += `'delete': ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['delete', origModelName]); - } - - if (deleteMany) { - imports.push( - `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'` - ); - const fields = `where: ${modelName}WhereInputObjectSchema.optional()`; - codeBody += `deleteMany: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['deleteMany', origModelName]); - } - - if (updateOne) { - imports.push( - `import { ${modelName}UpdateInputObjectSchema } from '../objects/${modelName}UpdateInput.schema'`, - `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'` - ); - if (generateUnchecked) { - imports.push( - `import { ${modelName}UncheckedUpdateInputObjectSchema } from '../objects/${modelName}UncheckedUpdateInput.schema'` - ); - } - const dataSchema = generateUnchecked - ? this.wrapWithSmartUnion( - `${modelName}UpdateInputObjectSchema`, - `${modelName}UncheckedUpdateInputObjectSchema` - ) - : `${modelName}UpdateInputObjectSchema`; - const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} data: ${dataSchema}, where: ${modelName}WhereUniqueInputObjectSchema`; - codeBody += `update: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['update', origModelName]); - } - - if (updateMany) { - imports.push( - `import { ${modelName}UpdateManyMutationInputObjectSchema } from '../objects/${modelName}UpdateManyMutationInput.schema'`, - `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'` - ); - if (generateUnchecked) { - imports.push( - `import { ${modelName}UncheckedUpdateManyInputObjectSchema } from '../objects/${modelName}UncheckedUpdateManyInput.schema'` - ); - } - const dataSchema = generateUnchecked - ? this.wrapWithSmartUnion( - `${modelName}UpdateManyMutationInputObjectSchema`, - `${modelName}UncheckedUpdateManyInputObjectSchema` - ) - : `${modelName}UpdateManyMutationInputObjectSchema`; - const fields = `data: ${dataSchema}, where: ${modelName}WhereInputObjectSchema.optional()`; - codeBody += `updateMany: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['updateMany', origModelName]); - } - - if (upsertOne) { - imports.push( - `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`, - `import { ${modelName}CreateInputObjectSchema } from '../objects/${modelName}CreateInput.schema'`, - `import { ${modelName}UpdateInputObjectSchema } from '../objects/${modelName}UpdateInput.schema'` - ); - if (generateUnchecked) { - imports.push( - `import { ${modelName}UncheckedCreateInputObjectSchema } from '../objects/${modelName}UncheckedCreateInput.schema'`, - `import { ${modelName}UncheckedUpdateInputObjectSchema } from '../objects/${modelName}UncheckedUpdateInput.schema'` - ); - } - const createSchema = generateUnchecked - ? this.wrapWithSmartUnion( - `${modelName}CreateInputObjectSchema`, - `${modelName}UncheckedCreateInputObjectSchema` - ) - : `${modelName}CreateInputObjectSchema`; - const updateSchema = generateUnchecked - ? this.wrapWithSmartUnion( - `${modelName}UpdateInputObjectSchema`, - `${modelName}UncheckedUpdateInputObjectSchema` - ) - : `${modelName}UpdateInputObjectSchema`; - const fields = `${selectZodSchemaLineLazy} ${includeZodSchemaLineLazy} where: ${modelName}WhereUniqueInputObjectSchema, create: ${createSchema}, update: ${updateSchema}`; - codeBody += `upsert: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['upsert', origModelName]); - } - - const aggregateOperations = []; - - if (this.aggregateOperationSupport[modelName]?.count) { - imports.push( - `import { ${modelName}CountAggregateInputObjectSchema } from '../objects/${modelName}CountAggregateInput.schema'` - ); - aggregateOperations.push( - `_count: z.union([ z.literal(true), ${modelName}CountAggregateInputObjectSchema ]).optional()` - ); - } - if (this.aggregateOperationSupport[modelName]?.min) { - imports.push( - `import { ${modelName}MinAggregateInputObjectSchema } from '../objects/${modelName}MinAggregateInput.schema'` - ); - aggregateOperations.push(`_min: ${modelName}MinAggregateInputObjectSchema.optional()`); - } - if (this.aggregateOperationSupport[modelName]?.max) { - imports.push( - `import { ${modelName}MaxAggregateInputObjectSchema } from '../objects/${modelName}MaxAggregateInput.schema'` - ); - aggregateOperations.push(`_max: ${modelName}MaxAggregateInputObjectSchema.optional()`); - } - if (this.aggregateOperationSupport[modelName]?.avg) { - imports.push( - `import { ${modelName}AvgAggregateInputObjectSchema } from '../objects/${modelName}AvgAggregateInput.schema'` - ); - aggregateOperations.push(`_avg: ${modelName}AvgAggregateInputObjectSchema.optional()`); - } - if (this.aggregateOperationSupport[modelName]?.sum) { - imports.push( - `import { ${modelName}SumAggregateInputObjectSchema } from '../objects/${modelName}SumAggregateInput.schema'` - ); - aggregateOperations.push(`_sum: ${modelName}SumAggregateInputObjectSchema.optional()`); - } - - if (aggregate) { - imports.push( - `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, - `import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`, - `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'` - ); - - const fields = `where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), ${aggregateOperations.join( - ', ' - )}`; - codeBody += `aggregate: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['aggregate', modelName]); - } - - if (groupBy) { - imports.push( - `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, - `import { ${modelName}OrderByWithAggregationInputObjectSchema } from '../objects/${modelName}OrderByWithAggregationInput.schema'`, - `import { ${modelName}ScalarWhereWithAggregatesInputObjectSchema } from '../objects/${modelName}ScalarWhereWithAggregatesInput.schema'`, - `import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'` - ); - const fields = `where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${modelName}OrderByWithAggregationInputObjectSchema, ${modelName}OrderByWithAggregationInputObjectSchema.array()]).optional(), having: ${modelName}ScalarWhereWithAggregatesInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), by: z.array(${modelName}ScalarFieldEnumSchema), ${aggregateOperations.join( - ', ' - )}`; - codeBody += `groupBy: ${this.wrapWithZodObject(fields, mode)},`; - - operations.push(['groupBy', origModelName]); - } - - // count - { - imports.push( - `import { ${modelName}WhereInputObjectSchema } from '../objects/${modelName}WhereInput.schema'`, - `import { ${orderByWithRelationInput}ObjectSchema } from '../objects/${orderByWithRelationInput}.schema'`, - `import { ${modelName}WhereUniqueInputObjectSchema } from '../objects/${modelName}WhereUniqueInput.schema'`, - `import { ${modelName}ScalarFieldEnumSchema } from '../enums/${modelName}ScalarFieldEnum.schema'`, - `import { ${modelName}CountAggregateInputObjectSchema } from '../objects/${modelName}CountAggregateInput.schema'` - ); - - const fields = `where: ${modelName}WhereInputObjectSchema.optional(), orderBy: z.union([${orderByWithRelationInput}ObjectSchema, ${orderByWithRelationInput}ObjectSchema.array()]).optional(), cursor: ${modelName}WhereUniqueInputObjectSchema.optional(), take: z.number().optional(), skip: z.number().optional(), distinct: z.array(${modelName}ScalarFieldEnumSchema).optional(), select: z.union([ z.literal(true), ${modelName}CountAggregateInputObjectSchema ]).optional()`; - codeBody += `count: ${this.wrapWithZodObject(fields, mode)},`; - operations.push(['count', origModelName]); - } - - imports = [...new Set(imports)]; - - const filePath = path.join(Transformer.outputPath, `input/${modelName}Input.schema.ts`); - const content = ` - ${imports.join(';\n')} - - type ${modelName}InputSchemaType = { -${operations - .map(([operation, typeName]) => { - const argType = `Prisma.${typeName}${upperCaseFirst(operation)}Args`; - return indentString(`${operation}: ${this.makeZodType(argType)}`, 4) -}) - .join(',\n')} - } - - export const ${modelName}InputSchema = { - ${indentString(codeBody, 4)} - } as ${modelName}InputSchemaType; - `; - - this.sourceFiles.push(this.project.createSourceFile(filePath, content, { overwrite: true })); - } - - const indexFilePath = path.join(Transformer.outputPath, 'input/index.ts'); - const indexContent = ` -${globalExports.join(';\n')} -`; - this.sourceFiles.push(this.project.createSourceFile(indexFilePath, indexContent, { overwrite: true })); - } - - generateImportStatements(imports: (string | undefined)[]) { - let generatedImports = this.generateImportZodStatement(); - generatedImports += imports?.filter((importItem) => !!importItem).join(';\r\n') ?? ''; - generatedImports += '\n\n'; - return generatedImports; - } - - resolveSelectIncludeImportAndZodSchemaLine(model: PrismaDMMF.Model) { - const { name } = model; - const modelName = upperCaseFirst(name); - - const hasRelationToAnotherModel = checkModelHasModelRelation(model); - - const selectImport = `import { ${modelName}SelectObjectSchema } from '../objects/${modelName}Select.schema'`; - - const includeImport = hasRelationToAnotherModel - ? `import { ${modelName}IncludeObjectSchema } from '../objects/${modelName}Include.schema'` - : ''; - - let selectZodSchemaLine = ''; - let includeZodSchemaLine = ''; - let selectZodSchemaLineLazy = ''; - let includeZodSchemaLineLazy = ''; - - const zodSelectObjectSchema = `${modelName}SelectObjectSchema.optional()`; - selectZodSchemaLine = `select: ${zodSelectObjectSchema},`; - selectZodSchemaLineLazy = `select: z.lazy(() => ${zodSelectObjectSchema}),`; - - if (hasRelationToAnotherModel) { - const zodIncludeObjectSchema = `${modelName}IncludeObjectSchema.optional()`; - includeZodSchemaLine = `include: ${zodIncludeObjectSchema},`; - includeZodSchemaLineLazy = `include: z.lazy(() => ${zodIncludeObjectSchema}),`; - } - - return { - selectImport, - includeImport, - selectZodSchemaLine, - includeZodSchemaLine, - selectZodSchemaLineLazy, - includeZodSchemaLineLazy, - }; - } - - private makeZodType(typeArg: string) { - return this.zodVersion === 'v3' ? `z.ZodType<${typeArg}>` : `z.ZodType<${typeArg}, ${typeArg}>`; - } -} diff --git a/packages/schema/src/plugins/zod/types.ts b/packages/schema/src/plugins/zod/types.ts deleted file mode 100644 index e0e1e03ba..000000000 --- a/packages/schema/src/plugins/zod/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Model } from '@zenstackhq/sdk/ast'; -import type { DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; -import { Project } from 'ts-morph'; - -export type TransformerParams = { - enumTypes?: readonly PrismaDMMF.SchemaEnum[]; - fields?: readonly PrismaDMMF.SchemaArg[]; - name?: string; - models?: readonly PrismaDMMF.Model[]; - modelOperations?: PrismaDMMF.ModelMapping[]; - aggregateOperationSupport?: AggregateOperationSupport; - isDefaultPrismaClientOutput?: boolean; - prismaClientOutputPath?: string; - project: Project; - inputObjectTypes: PrismaDMMF.InputType[]; - zmodel: Model; - mode: ObjectMode; - zodVersion: 'v3' | 'v4'; -}; - -export type AggregateOperationSupport = { - [model: string]: { - count?: boolean; - min?: boolean; - max?: boolean; - sum?: boolean; - avg?: boolean; - }; -}; - -export type ObjectMode = 'strict' | 'strip' | 'passthrough'; diff --git a/packages/schema/src/plugins/zod/utils/schema-gen.ts b/packages/schema/src/plugins/zod/utils/schema-gen.ts deleted file mode 100644 index e0e0baf58..000000000 --- a/packages/schema/src/plugins/zod/utils/schema-gen.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { getLiteral, hasAttribute, isEnumFieldReference, isFromStdlib } from '@zenstackhq/sdk'; -import { - DataModelField, - DataModelFieldAttribute, - isBooleanLiteral, - isDataModel, - isEnum, - isInvocationExpr, - isNumberLiteral, - isStringLiteral, - isTypeDef, - TypeDefField, -} from '@zenstackhq/sdk/ast'; -import { isDefaultWithAuth } from '../../enhancer/enhancer-utils'; - -export function makeFieldSchema(field: DataModelField | TypeDefField, addDefaults: boolean = true) { - if (isDataModel(field.type.reference?.ref)) { - if (field.type.array) { - // array field is always optional - return `z.array(z.unknown()).optional()`; - } else { - return field.type.optional - ? `z.record(z.string(), z.unknown()).optional()` - : `z.record(z.string(), z.unknown())`; - } - } - - let schema = makeZodSchema(field); - const isDecimal = field.type.type === 'Decimal'; - - for (const attr of field.attributes) { - const message = getAttrLiteralArg(attr, 'message'); - const messageArg = message ? `, { message: ${JSON.stringify(message)} }` : ''; - const messageArgFirst = message ? `{ message: ${JSON.stringify(message)} }` : ''; - - switch (attr.decl.ref?.name) { - case '@length': { - const min = getAttrLiteralArg(attr, 'min'); - if (min) { - schema += `.min(${min}${messageArg})`; - } - const max = getAttrLiteralArg(attr, 'max'); - if (max) { - schema += `.max(${max}${messageArg})`; - } - break; - } - case '@contains': { - const expr = getAttrLiteralArg(attr, 'text'); - if (expr) { - schema += `.includes(${JSON.stringify(expr)}${messageArg})`; - } - break; - } - case '@regex': { - const expr = getAttrLiteralArg(attr, 'regex'); - if (expr) { - schema += `.regex(new RegExp(${JSON.stringify(expr)})${messageArg})`; - } - break; - } - case '@startsWith': { - const text = getAttrLiteralArg(attr, 'text'); - if (text) { - schema += `.startsWith(${JSON.stringify(text)}${messageArg})`; - } - break; - } - case '@endsWith': { - const text = getAttrLiteralArg(attr, 'text'); - if (text) { - schema += `.endsWith(${JSON.stringify(text)}${messageArg})`; - } - break; - } - case '@email': { - schema += `.email(${messageArgFirst})`; - break; - } - case '@url': { - schema += `.url(${messageArgFirst})`; - break; - } - case '@trim': { - schema += `.trim()`; - break; - } - case '@lower': { - schema += `.toLowerCase()`; - break; - } - case '@upper': { - schema += `.toUpperCase()`; - break; - } - case '@db.Uuid': { - schema += `.uuid()`; - break; - } - case '@datetime': { - schema += `.datetime({ offset: true${message ? ', message: ' + JSON.stringify(message) : ''} })`; - break; - } - case '@gt': { - const value = getAttrLiteralArg(attr, 'value'); - if (value !== undefined) { - schema += isDecimal ? refineDecimal('gt', value, messageArg) : `.gt(${value}${messageArg})`; - } - break; - } - case '@gte': { - const value = getAttrLiteralArg(attr, 'value'); - if (value !== undefined) { - schema += isDecimal ? refineDecimal('gte', value, messageArg) : `.gte(${value}${messageArg})`; - } - break; - } - case '@lt': { - const value = getAttrLiteralArg(attr, 'value'); - if (value !== undefined) { - schema += isDecimal ? refineDecimal('lt', value, messageArg) : `.lt(${value}${messageArg})`; - } - break; - } - case '@lte': { - const value = getAttrLiteralArg(attr, 'value'); - if (value !== undefined) { - schema += isDecimal ? refineDecimal('lte', value, messageArg) : `.lte(${value}${messageArg})`; - } - break; - } - } - } - - if (field.attributes.some(isDefaultWithAuth)) { - if (field.type.optional) { - schema += '.nullish()'; - } else { - // field uses `auth()` in `@default()`, this was transformed into a pseudo default - // value, while compiling to zod we should turn it into an optional field instead - // of `.default()` - schema += '.optional()'; - } - } else { - if (addDefaults) { - const schemaDefault = getFieldSchemaDefault(field); - if (schemaDefault !== undefined) { - if (field.type.type === 'BigInt') { - // we can't use the `n` BigInt literal notation, since it needs - // ES2020 or later, which TypeScript doesn't use by default - schema += `.default(BigInt("${schemaDefault}"))`; - } else { - schema += `.default(${schemaDefault})`; - } - } - } - - if (field.type.optional) { - schema += '.nullish()'; - } - } - - return schema; -} - -function makeZodSchema(field: DataModelField | TypeDefField) { - let schema: string; - - if (field.type.reference?.ref) { - if (isEnum(field.type.reference?.ref)) { - schema = `${upperCaseFirst(field.type.reference.ref.name)}Schema`; - } else if (isTypeDef(field.type.reference?.ref)) { - schema = `z.lazy(() => ${upperCaseFirst(field.type.reference.ref.name)}Schema)`; - } else { - schema = 'z.any()'; - } - } else { - switch (field.type.type) { - case 'Int': - case 'Float': - schema = 'z.number()'; - break; - case 'Decimal': - schema = 'DecimalSchema'; - break; - case 'BigInt': - schema = 'z.bigint()'; - break; - case 'String': - schema = 'z.string()'; - break; - case 'Boolean': - schema = 'z.boolean()'; - break; - case 'DateTime': - schema = 'z.coerce.date()'; - break; - case 'Bytes': - schema = 'z.union([z.string(), z.custom(data => data instanceof Uint8Array)])'; - break; - default: - schema = 'z.any()'; - break; - } - } - - if (field.type.array) { - schema = `z.array(${schema})`; - } - - return schema; -} - -function getAttrLiteralArg(attr: DataModelFieldAttribute, paramName: string) { - const arg = attr.args.find((arg) => arg.$resolvedParam?.name === paramName); - return arg && getLiteral(arg.value); -} - -function refineDecimal(op: 'gt' | 'gte' | 'lt' | 'lte', value: number, messageArg: string) { - return `.refine(v => { - try { - return new Decimal(v.toString()).${op}(${value}); - } catch { - return false; - } - }${messageArg})`; -} - -export function getFieldSchemaDefault(field: DataModelField | TypeDefField) { - const attr = field.attributes.find((attr) => attr.decl.ref?.name === '@default'); - if (!attr) { - return undefined; - } - const arg = attr.args.find((arg) => arg.$resolvedParam?.name === 'value'); - if (arg) { - if (isStringLiteral(arg.value)) { - if (field.type.type === 'Json' || hasAttribute(field, '@json')) { - // JSON field can have JSON string as default value, we shouldn't - // stringify it back - return arg.value.value; - } else { - return JSON.stringify(arg.value.value); - } - } else if (isNumberLiteral(arg.value)) { - return arg.value.value; - } else if (isBooleanLiteral(arg.value)) { - return arg.value.value; - } else if (isEnumFieldReference(arg.value) && arg.value.target.ref) { - return JSON.stringify(arg.value.target.ref.name); - } else if ( - isInvocationExpr(arg.value) && - isFromStdlib(arg.value.function.ref!) && - arg.value.function.$refText === 'now' - ) { - return `() => new Date()`; - } - } - - return undefined; -} diff --git a/packages/schema/src/res/starter.txt b/packages/schema/src/res/starter.txt deleted file mode 100644 index 978724dc7..000000000 --- a/packages/schema/src/res/starter.txt +++ /dev/null @@ -1,43 +0,0 @@ -// This is a sample model to get you started. - -/// A sample data source using local sqlite db. -datasource db { - provider = 'sqlite' - url = 'file:./dev.db' -} - -generator client { - provider = "prisma-client-js" -} - -/// User model -model User { - id String @id @default(cuid()) - email String @unique @email @length(6, 32) - password String @password @omit - posts Post[] - - // everybody can signup - @@allow('create', true) - - // full access by self - @@allow('all', auth() == this) -} - -/// Post model -model Post { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String @length(1, 256) - content String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String - - // allow read for all signin users - @@allow('read', auth() != null && published) - - // full access by author - @@allow('all', author == auth()) -} diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel deleted file mode 100644 index 1fde1fc57..000000000 --- a/packages/schema/src/res/stdlib.zmodel +++ /dev/null @@ -1,761 +0,0 @@ -/** - * Enum representing referential integrity related actions - * @see https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions - */ -enum ReferentialAction { - /** - * Used with "onDelete": deleting a referenced record will trigger the deletion of referencing record. - * Used with "onUpdate": updates the relation scalar fields if the referenced scalar fields of the dependent record are updated. - */ - Cascade - - /** - * Used with "onDelete": prevents the deletion if any referencing records exist. - * Used with "onUpdate": prevents the identifier of a referenced record from being changed. - */ - Restrict - - /** - * Similar to 'Restrict', the difference between the two is dependent on the database being used. - */ - NoAction - - /** - * Used with "onDelete": the scalar field of the referencing object will be set to NULL. - * Used with "onUpdate": when updating the identifier of a referenced object, the scalar fields of the referencing objects will be set to NULL. - */ - SetNull - - /** - * Used with "onDelete": the scalar field of the referencing object will be set to the fields default value. - * Used with "onUpdate": the scalar field of the referencing object will be set to the fields default value. - */ - SetDefault -} - -/** - * Enum representing all possible field types - */ -enum AttributeTargetField { - StringField - IntField - BigIntField - FloatField - DecimalField - BooleanField - DateTimeField - JsonField - BytesField - ModelField - TypeDefField -} - -/** - * Indicates the expression context a function can be used. - */ -enum ExpressionContext { - // used in @default - DefaultValue - - // used in @@allow and @@deny - AccessPolicy - - // used in @@validate - ValidationRule - - // used in @@index - Index -} - -/** - * Reads value from an environment variable. - */ -function env(name: String): String { -} - -/** - * Gets the current login user. - */ -function auth(): Any { -} @@@expressionContext([DefaultValue, AccessPolicy]) - -/** - * Gets current date-time (as DateTime type). - */ -function now(): DateTime { -} @@@expressionContext([DefaultValue, AccessPolicy, ValidationRule]) - -/** - * Generates a globally unique identifier based on the UUID specs. - */ -function uuid(version: Int?): String { -} @@@expressionContext([DefaultValue]) - -/** - * Generates a globally unique identifier based on the CUID spec. - */ -function cuid(version: Int?): String { -} @@@expressionContext([DefaultValue]) - -/** - * Generates an identifier based on the nanoid spec. - */ -function nanoid(length: Int?): String { -} @@@expressionContext([DefaultValue]) - -/** - * Generates an identifier based on the ulid spec. - */ -function ulid(): String { -} @@@expressionContext([DefaultValue]) - -/** - * Creates a sequence of integers in the underlying database and assign the incremented - * values to the ID values of the created records based on the sequence. - */ -function autoincrement(): Int { -} @@@expressionContext([DefaultValue]) - -/** - * Represents default values that cannot be expressed in the Prisma schema (such as random()). - */ -function dbgenerated(expr: String?): Any { -} @@@expressionContext([DefaultValue]) - -/** - * Gets entities value before an update. Only valid when used in a "update" policy rule. - */ -function future(): Any { -} @@@expressionContext([AccessPolicy]) - -/** - * If the field value contains the search string. By default, the search is case-sensitive, - * but you can override the behavior with the "caseInSensitive" argument. - */ -function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean { -} @@@expressionContext([AccessPolicy, ValidationRule]) - -/** - * If the field value matches the search condition with [full-text-search](https://www.prisma.io/docs/concepts/components/prisma-client/full-text-search). Need to enable "fullTextSearch" preview feature to use. - */ -function search(field: String, search: String): Boolean { -} @@@expressionContext([AccessPolicy]) - -/** - * If the field value starts with the search string - */ -function startsWith(field: String, search: String): Boolean { -} @@@expressionContext([AccessPolicy, ValidationRule]) - -/** - * If the field value ends with the search string - */ -function endsWith(field: String, search: String): Boolean { -} @@@expressionContext([AccessPolicy, ValidationRule]) - -/** - * If the field value (a list) has the given search value - */ -function has(field: Any[], search: Any): Boolean { -} @@@expressionContext([AccessPolicy, ValidationRule]) - -/** - * If the field value (a list) has every element of the search list - */ -function hasEvery(field: Any[], search: Any[]): Boolean { -} @@@expressionContext([AccessPolicy, ValidationRule]) - -/** - * If the field value (a list) has at least one element of the search list - */ -function hasSome(field: Any[], search: Any[]): Boolean { -} @@@expressionContext([AccessPolicy, ValidationRule]) - -/** - * If the field value (a list) is empty - */ -function isEmpty(field: Any[]): Boolean { -} @@@expressionContext([AccessPolicy, ValidationRule]) - -/** - * The name of the model for which the policy rule is defined. If the rule is - * inherited to a sub model, this function returns the name of the sub model. - * - * @param optional parameter to control the casing of the returned value. Valid - * values are "original", "upper", "lower", "capitalize", "uncapitalize". Defaults - * to "original". - */ -function currentModel(casing: String?): String { -} @@@expressionContext([AccessPolicy]) - -/** - * The operation for which the policy rule is defined for. Note that a rule with - * "all" operation is expanded to "create", "read", "update", and "delete" rules, - * and the function returns corresponding value for each expanded version. - * - * @param optional parameter to control the casing of the returned value. Valid - * values are "original", "upper", "lower", "capitalize", "uncapitalize". Defaults - * to "original". - */ -function currentOperation(casing: String?): String { -} @@@expressionContext([AccessPolicy]) - -/** - * Marks an attribute to be only applicable to certain field types. - */ -attribute @@@targetField(_ targetField: AttributeTargetField[]) - -/** - * Marks an attribute to be applicable to type defs and fields. - */ -attribute @@@supportTypeDef() - -/** - * Marks an attribute to be used for data validation. - */ -attribute @@@validation() - -/** - * Indicates the expression context a function can be used. - */ -attribute @@@expressionContext(_ context: ExpressionContext[]) - -/** - * Indicates an attribute is directly supported by the Prisma schema. - */ -attribute @@@prisma() - -/** - * Provides hint for auto-completion. - */ -attribute @@@completionHint(_ values: String[]) - -/** - * Defines a single-field ID on the model. - * - * @param map: The name of the underlying primary key constraint in the database. - * @param length: Allows you to specify a maximum length for the subpart of the value to be indexed. - * @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc. - * @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true. - */ -attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma @@@supportTypeDef - -/** - * Defines a default value for a field. - * @param value: An expression (e.g. 5, true, now(), auth()). - */ -attribute @default(_ value: ContextType, map: String?) @@@prisma @@@supportTypeDef - -/** - * Defines a unique constraint for this field. - * - * @param length: Allows you to specify a maximum length for the subpart of the value to be indexed. - * @param sort: Allows you to specify in what order the entries of the constraint are stored in the database. The available options are Asc and Desc. - * @param clustered: Boolean Defines whether the constraint is clustered or non-clustered. Defaults to false. - */ -attribute @unique(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma - -/** - * Defines a multi-field ID (composite ID) on the model. - * - * @param fields: A list of field names - for example, [firstname, lastname] - * @param name: The name that Prisma Client will expose for the argument covering all fields, e.g. fullName in fullName: { firstName: "First", lastName: "Last"} - * @param map: The name of the underlying primary key constraint in the database. - * @param length: Allows you to specify a maximum length for the subpart of the value to be indexed. - * @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc. - * @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true. - */ -attribute @@id(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma - -/** - * Defines a compound unique constraint for the specified fields. - * - * @param fields: A list of field names - for example, [firstname, lastname]. Fields must be mandatory. - * @param name: The name of the unique combination of fields - defaults to fieldName1_fieldName2_fieldName3 - * @param length: Allows you to specify a maximum length for the subpart of the value to be indexed. - * @param sort: Allows you to specify in what order the entries of the constraint are stored in the database. The available options are Asc and Desc. - * @param clustered: Boolean Defines whether the constraint is clustered or non-clustered. Defaults to false. - */ -attribute @@unique(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma - -/** - * Index types - */ -enum IndexType { - BTree - Hash - Gist - Gin - SpGist - Brin -} - -/** - * Operator class for index - */ -enum IndexOperatorClass { - // GIN - ArrayOps - JsonbOps - JsonbPathOps - - // Gist - InetOps - - // SpGist - TextOps - - // BRIN - BitMinMaxOps - VarBitMinMaxOps - BpcharBloomOps - BpcharMinMaxOps - ByteaBloomOps - ByteaMinMaxOps - DateBloomOps - DateMinMaxOps - DateMinMaxMultiOps - Float4BloomOps - Float4MinMaxOps - Float4MinMaxMultiOps - Float8BloomOps - Float8MinMaxOps - Float8MinMaxMultiOps - InetInclusionOps - InetBloomOps - InetMinMaxOps - InetMinMaxMultiOps - Int2BloomOps - Int2MinMaxOps - Int2MinMaxMultiOps - Int4BloomOps - Int4MinMaxOps - Int4MinMaxMultiOps - Int8BloomOps - Int8MinMaxOps - Int8MinMaxMultiOps - NumericBloomOps - NumericMinMaxOps - NumericMinMaxMultiOps - OidBloomOps - OidMinMaxOps - OidMinMaxMultiOps - TextBloomOps - TextMinMaxOps - TextMinMaxMultiOps - TimestampBloomOps - TimestampMinMaxOps - TimestampMinMaxMultiOps - TimestampTzBloomOps - TimestampTzMinMaxOps - TimestampTzMinMaxMultiOps - TimeBloomOps - TimeMinMaxOps - TimeMinMaxMultiOps - TimeTzBloomOps - TimeTzMinMaxOps - TimeTzMinMaxMultiOps - UuidBloomOps - UuidMinMaxOps - UuidMinMaxMultiOps -} - -/** - * Index sort order - */ -enum SortOrder { - Asc - Desc -} - -/** - * Defines an index in the database. - * - * @params fields: A list of field names - for example, [firstname, lastname] - * @params name: The name that Prisma Client will expose for the argument covering all fields, e.g. fullName in fullName: { firstName: "First", lastName: "Last"} - * @params map: The name of the index in the underlying database (Prisma generates an index name that respects identifier length limits if you do not specify a name. Prisma uses the following naming convention: tablename.field1_field2_field3_unique) - * @params length: Allows you to specify a maximum length for the subpart of the value to be indexed. - * @params sort: Allows you to specify in what order the entries of the index or constraint are stored in the database. The available options are asc and desc. - * @params clustered: Defines whether the index is clustered or non-clustered. Defaults to false. - * @params type: Allows you to specify an index access method. Defaults to BTree. - */ -attribute @@index(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?, type: IndexType?) @@@prisma - -/** - * Defines meta information about the relation. - * - * @param name: Sometimes (e.g. to disambiguate a relation) Defines the name of the relationship. In an m-n-relation, it also determines the name of the underlying relation table. - * @param fields: A list of fields of the current model - * @param references: A list of fields of the model on the other side of the relation - * @param map: Defines a custom name for the foreign key in the database. - * @param onUpdate: Defines the referential action to perform when a referenced entry in the referenced model is being updated. - * @param onDelete: Defines the referential action to perform when a referenced entry in the referenced model is being deleted. - */ -attribute @relation(_ name: String?, fields: FieldReference[]?, references: TransitiveFieldReference[]?, onDelete: ReferentialAction?, onUpdate: ReferentialAction?, map: String?) @@@prisma - -/** - * Maps a field name or enum value from the schema to a column with a different name in the database. - * - * @param name: The database column name. - */ -attribute @map(_ name: String) @@@prisma - -/** - * Maps the schema model name to a table with a different name, or an enum name to a different underlying enum in the database. - * - * @param name: The database column name. - */ -attribute @@map(_ name: String) @@@prisma - -/** - * Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update). - */ -attribute @ignore() @@@prisma - -/** - * Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update). - */ -attribute @@ignore() @@@prisma - -/** - * Automatically stores the time when a record was last updated. - */ -attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma - -/** - * Add full text index (MySQL only). - */ -attribute @@fulltext(_ fields: FieldReference[], map: String?) @@@prisma - - -// String type modifiers - -enum MSSQLServerTypes { - Max -} - -attribute @db.String(_ x: Int?) @@@targetField([StringField]) @@@prisma -attribute @db.Text() @@@targetField([StringField]) @@@prisma -attribute @db.NText() @@@targetField([StringField]) @@@prisma -attribute @db.Char(_ x: Int?) @@@targetField([StringField]) @@@prisma -attribute @db.NChar(_ x: Int?) @@@targetField([StringField]) @@@prisma -attribute @db.VarChar(_ x: Any?) @@@targetField([StringField]) @@@prisma -attribute @db.NVarChar(_ x: Any?) @@@targetField([StringField]) @@@prisma -attribute @db.CatalogSingleChar() @@@targetField([StringField]) @@@prisma -attribute @db.TinyText() @@@targetField([StringField]) @@@prisma -attribute @db.MediumText() @@@targetField([StringField]) @@@prisma -attribute @db.LongText() @@@targetField([StringField]) @@@prisma -attribute @db.Bit(_ x: Int?) @@@targetField([StringField, BooleanField, BytesField]) @@@prisma -attribute @db.VarBit(_ x: Int?) @@@targetField([StringField]) @@@prisma -attribute @db.Uuid() @@@targetField([StringField]) @@@prisma -attribute @db.UniqueIdentifier() @@@targetField([StringField]) @@@prisma -attribute @db.Xml() @@@targetField([StringField]) @@@prisma -attribute @db.Inet() @@@targetField([StringField]) @@@prisma -attribute @db.Citext() @@@targetField([StringField]) @@@prisma - -// Boolean type modifiers - -attribute @db.Boolean() @@@targetField([BooleanField]) @@@prisma -attribute @db.TinyInt(_ x: Int?) @@@targetField([BooleanField, IntField]) @@@prisma -attribute @db.Bool() @@@targetField([BooleanField]) @@@prisma - -// Int type modifiers - -attribute @db.Int() @@@targetField([IntField]) @@@prisma -attribute @db.Integer() @@@targetField([IntField]) @@@prisma -attribute @db.SmallInt() @@@targetField([IntField]) @@@prisma -attribute @db.Oid() @@@targetField([IntField]) @@@prisma -attribute @db.UnsignedInt() @@@targetField([IntField]) @@@prisma -attribute @db.UnsignedSmallInt() @@@targetField([IntField]) @@@prisma -attribute @db.MediumInt() @@@targetField([IntField]) @@@prisma -attribute @db.UnsignedMediumInt() @@@targetField([IntField]) @@@prisma -attribute @db.UnsignedTinyInt() @@@targetField([IntField]) @@@prisma -attribute @db.Year() @@@targetField([IntField]) @@@prisma -attribute @db.Int4() @@@targetField([IntField]) @@@prisma -attribute @db.Int2() @@@targetField([IntField]) @@@prisma - -// BigInt type modifiers - -attribute @db.BigInt() @@@targetField([BigIntField]) @@@prisma -attribute @db.UnsignedBigInt() @@@targetField([BigIntField]) @@@prisma -attribute @db.Int8() @@@targetField([BigIntField]) @@@prisma - -// Float/Decimal type modifiers -attribute @db.DoublePrecision() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Real() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Float() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Decimal(_ p: Int?, _ s: Int?) @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Double() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Money() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.SmallMoney() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Float8() @@@targetField([FloatField, DecimalField]) @@@prisma -attribute @db.Float4() @@@targetField([FloatField, DecimalField]) @@@prisma - -// DateTime type modifiers - -attribute @db.DateTime(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma -attribute @db.DateTime2() @@@targetField([DateTimeField]) @@@prisma -attribute @db.SmallDateTime() @@@targetField([DateTimeField]) @@@prisma -attribute @db.DateTimeOffset() @@@targetField([DateTimeField]) @@@prisma -attribute @db.Timestamp(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma -attribute @db.Timestamptz(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma -attribute @db.Date() @@@targetField([DateTimeField]) @@@prisma -attribute @db.Time(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma -attribute @db.Timetz(_ x: Int?) @@@targetField([DateTimeField]) @@@prisma - -// Json type modifiers - -attribute @db.Json() @@@targetField([JsonField, TypeDefField]) @@@prisma -attribute @db.JsonB() @@@targetField([JsonField, TypeDefField]) @@@prisma - -// Bytes type modifiers - -attribute @db.Bytes() @@@targetField([BytesField]) @@@prisma -attribute @db.ByteA() @@@targetField([BytesField]) @@@prisma -attribute @db.LongBlob() @@@targetField([BytesField]) @@@prisma -attribute @db.Binary() @@@targetField([BytesField]) @@@prisma -attribute @db.VarBinary(_ x: Int?) @@@targetField([BytesField]) @@@prisma -attribute @db.TinyBlob() @@@targetField([BytesField]) @@@prisma -attribute @db.Blob() @@@targetField([BytesField]) @@@prisma -attribute @db.MediumBlob() @@@targetField([BytesField]) @@@prisma -attribute @db.Image() @@@targetField([BytesField]) @@@prisma - -/** - * Specifies the schema to use in a multi-schema database. https://www.prisma.io/docs/guides/database/multi-schema. - * - * @param: The name of the database schema. - */ -attribute @@schema(_ name: String) @@@prisma - -/** - * Defines an access policy that allows a set of operations when the given condition is true. - * - * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. - * @param condition: a boolean expression that controls if the operation should be allowed. - */ -attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) - -/** - * Defines an access policy that allows the annotated field to be read or updated. - * You can pass a third argument as `true` to make it override the model-level policies. - * - * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. - * @param condition: a boolean expression that controls if the operation should be allowed. - * @param override: a boolean value that controls if the field-level policy should override the model-level policy. - */ -attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean, _ override: Boolean?) - -/** - * Defines an access policy that denies a set of operations when the given condition is true. - * - * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. - * @param condition: a boolean expression that controls if the operation should be denied. - */ -attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) - -/** - * Defines an access policy that denies the annotated field to be read or updated. - * - * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. - * @param condition: a boolean expression that controls if the operation should be denied. - */ -attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) - -/** - * Used to specify the model for resolving `auth()` function call in access policies. A Zmodel - * can have at most one model with this attribute. By default, the model named "User" is used. - */ -attribute @@auth() @@@supportTypeDef - -/** - * Indicates that the field is a password field and needs to be hashed before persistence. - * - * ZenStack uses `bcryptjs` library to hash password. You can use the `saltLength` parameter - * to configure the cost of hashing, or use `salt` parameter to provide an explicit salt. - * By default, salt length of 12 is used. - * - * @see https://www.npmjs.com/package/bcryptjs for details - * - * @param saltLength: length of salt to use (cost factor for the hash function) - * @param salt: salt to use (a pregenerated valid salt) - */ -attribute @password(saltLength: Int?, salt: String?) @@@targetField([StringField]) - - -/** - * Indicates that the field is encrypted when storing in the DB and should be decrypted when read - * - * ZenStack uses the Web Crypto API to encrypt and decrypt the field. - */ -attribute @encrypted() @@@targetField([StringField]) - -/** - * Indicates that the field should be omitted when read from the generated services. - */ -attribute @omit() - -////////////////////////////////////////////// -// Begin validation attributes and functions -////////////////////////////////////////////// - -/** - * Validates length of a string field. - */ -attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Validates a string field value starts with the given text. - */ -attribute @startsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Validates a string field value ends with the given text. - */ -attribute @endsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Validates a string field value contains the given text. - */ -attribute @contains(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Validates a string field value matches a regex. - */ -attribute @regex(_ regex: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Validates a string field value is a valid email address. - */ -attribute @email(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Validates a string field value is a valid ISO datetime. - */ -attribute @datetime(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Validates a string field value is a valid url. - */ -attribute @url(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Trims whitespaces from the start and end of the string. - */ -attribute @trim() @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Transform entire string toLowerCase. - */ -attribute @lower() @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Transform entire string toUpperCase. - */ -attribute @upper() @@@targetField([StringField]) @@@validation @@@supportTypeDef - -/** - * Validates a number field is greater than the given value. - */ -attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef - -/** - * Validates a number field is greater than or equal to the given value. - */ -attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef - -/** - * Validates a number field is less than the given value. - */ -attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef - -/** - * Validates a number field is less than or equal to the given value. - */ -attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef - -/** - * Validates the entity with a complex condition. - */ -attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation @@@supportTypeDef - -/** - * Validates length of a string field. - */ -function length(field: String, min: Int, max: Int?): Boolean { -} @@@expressionContext([ValidationRule]) - - -/** - * Validates a string field value matches a regex. - */ -function regex(field: String, regex: String): Boolean { -} @@@expressionContext([ValidationRule]) - -/** - * Validates a string field value is a valid email address. - */ -function email(field: String): Boolean { -} @@@expressionContext([ValidationRule]) - -/** - * Validates a string field value is a valid ISO datetime. - */ -function datetime(field: String): Boolean { -} @@@expressionContext([ValidationRule]) - -/** - * Validates a string field value is a valid url. - */ -function url(field: String): Boolean { -} @@@expressionContext([ValidationRule]) - -/** - * Checks if the current user can perform the given operation on the given field. - * - * @param field: The field to check access for - * @param operation: The operation to check access for. Can be "read", "create", "update", or "delete". If the operation is not provided, - * it defaults the operation of the containing policy rule. - */ -function check(field: Any, operation: String?): Boolean { -} @@@expressionContext([AccessPolicy]) - -////////////////////////////////////////////// -// End validation attributes and functions -////////////////////////////////////////////// - -/** - * A utility attribute to allow passthrough of arbitrary attribute text to the generated Prisma schema. - */ -attribute @prisma.passthrough(_ text: String) - -/** - * A utility attribute to allow passthrough of arbitrary attribute text to the generated Prisma schema. - */ -attribute @@prisma.passthrough(_ text: String) - -/** - * Marks a model to be a delegate. Used for implementing polymorphism. - */ -attribute @@delegate(_ discriminator: FieldReference) - -/** - * Used for specifying operator classes for GIN index. - */ -function raw(value: String): Any { -} @@@expressionContext([Index]) - -/** - * Marks a field to be strong-typed JSON. - */ -attribute @json() @@@targetField([TypeDefField]) - - -/** - * Attaches arbitrary metadata to a model or type def. - */ -attribute @@meta(_ name: String, _ value: Any) - -/** - * Attaches arbitrary metadata to a field. - */ -attribute @meta(_ name: String, _ value: Any) diff --git a/packages/schema/src/telemetry.ts b/packages/schema/src/telemetry.ts deleted file mode 100644 index 9e9a764d7..000000000 --- a/packages/schema/src/telemetry.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { createId } from '@paralleldrive/cuid2'; -import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; -import { sleep } from '@zenstackhq/runtime/local-helpers'; -import exitHook from 'async-exit-hook'; -import { CommanderError } from 'commander'; -import { init, Mixpanel } from 'mixpanel'; -import * as os from 'os'; -import { CliError } from './cli/cli-error'; -import { TELEMETRY_TRACKING_TOKEN } from './constants'; -import isDocker from './utils/is-docker'; -import { isWsl } from './utils/is-wsl'; -import { getMachineId } from './utils/machine-id-utils'; -import { getVersion } from './utils/version-utils'; -import { isInCi } from './utils/is-ci'; -import { isInContainer } from './utils/is-container'; - -/** - * Telemetry events - */ -export type TelemetryEvents = - | 'cli:start' - | 'cli:complete' - | 'cli:error' - | 'cli:crash' - | 'cli:command:start' - | 'cli:command:complete' - | 'cli:command:error' - | 'cli:plugin:start' - | 'cli:plugin:complete' - | 'cli:plugin:error' - | 'prisma:error'; - -/** - * Utility class for sending telemetry - */ -export class Telemetry { - private readonly mixpanel: Mixpanel | undefined; - private readonly hostId = getMachineId(); - private readonly sessionid = createId(); - private readonly _os_type = os.type(); - private readonly _os_release = os.release(); - private readonly _os_arch = os.arch(); - private readonly _os_version = os.version(); - private readonly _os_platform = os.platform(); - private readonly version = getVersion(); - private readonly prismaVersion = getPrismaVersion(); - private readonly isDocker = isDocker(); - private readonly isWsl = isWsl(); - private readonly isContainer = isInContainer(); - private readonly isCi = isInCi; - private exitWait = 200; - - constructor() { - if (process.env.DO_NOT_TRACK !== '1' && TELEMETRY_TRACKING_TOKEN) { - this.mixpanel = init(TELEMETRY_TRACKING_TOKEN, { - geolocate: true, - }); - } - - exitHook(async (callback) => { - if (this.mixpanel) { - // a small delay to ensure telemetry is sent - await sleep(this.exitWait); - } - callback(); - }); - - const errorHandler = async (err: Error) => { - if (err instanceof CliError || err instanceof CommanderError) { - this.track('cli:error', { - message: err.message, - stack: err.stack, - }); - if (this.mixpanel) { - // a small delay to ensure telemetry is sent - await sleep(this.exitWait); - } - // error already logged - } else { - console.error('\nAn unexpected error occurred:\n', err); - this.track('cli:crash', { - message: err.message, - stack: err.stack, - }); - if (this.mixpanel) { - // a small delay to ensure telemetry is sent - await sleep(this.exitWait); - } - } - - process.exit(1); - }; - - exitHook.unhandledRejectionHandler(errorHandler); - exitHook.uncaughtExceptionHandler(errorHandler); - } - - track(event: TelemetryEvents, properties: Record = {}) { - if (this.mixpanel) { - const payload = { - distinct_id: this.hostId, - session: this.sessionid, - time: new Date(), - $os: this._os_type, - osType: this._os_type, - osRelease: this._os_release, - osPlatform: this._os_platform, - osArch: this._os_arch, - osVersion: this._os_version, - nodeVersion: process.version, - version: this.version, - prismaVersion: this.prismaVersion, - isDocker: this.isDocker, - isWsl: this.isWsl, - isContainer: this.isContainer, - isCi: this.isCi, - ...properties, - }; - this.mixpanel.track(event, payload); - } - } - - async trackSpan( - startEvent: TelemetryEvents, - completeEvent: TelemetryEvents, - errorEvent: TelemetryEvents, - properties: Record, - action: () => Promise | T - ) { - this.track(startEvent, properties); - const start = Date.now(); - let success = true; - try { - return await action(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - this.track(errorEvent, { - message: err.message, - stack: err.stack, - ...properties, - }); - success = false; - throw err; - } finally { - this.track(completeEvent, { - duration: Date.now() - start, - success, - ...properties, - }); - } - } -} - -export default new Telemetry(); diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts deleted file mode 100644 index f59ee7faa..000000000 --- a/packages/schema/src/utils/ast-utils.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { - BinaryExpr, - DataModel, - DataModelAttribute, - DataModelField, - Expression, - InheritableNode, - isBinaryExpr, - isDataModel, - isDataModelField, - isInvocationExpr, - isModel, - isReferenceExpr, - isTypeDef, - Model, - ModelImport, - TypeDef, -} from '@zenstackhq/language/ast'; -import { - getAttribute, - getInheritanceChain, - getRecursiveBases, - hasAttribute, - isDelegateModel, - isFromStdlib, -} from '@zenstackhq/sdk'; -import { - AstNode, - copyAstNode, - CstNode, - getContainerOfType, - getDocument, - LangiumDocuments, - linkContentToContainer, - Linker, - Mutable, - Reference, -} from 'langium'; -import path from 'node:path'; -import { URI, Utils } from 'vscode-uri'; -import { findNodeModulesFile } from './pkg-utils'; - -export function extractDataModelsWithAllowRules(model: Model): DataModel[] { - return model.declarations.filter( - (d) => isDataModel(d) && d.attributes.some((attr) => attr.decl.ref?.name === '@@allow') - ) as DataModel[]; -} - -type BuildReference = ( - node: AstNode, - property: string, - refNode: CstNode | undefined, - refText: string -) => Reference; - -export function mergeBaseModels(model: Model, linker: Linker) { - const buildReference = linker.buildReference.bind(linker); - - model.declarations.filter(isDataModel).forEach((dataModel) => { - const bases = getRecursiveBases(dataModel).reverse(); - if (bases.length > 0) { - dataModel.fields = bases - .flatMap((base) => base.fields) - // don't inherit skip-level fields - .filter((f) => !f.$inheritedFrom) - .map((f) => cloneAst(f, dataModel, buildReference)) - .concat(dataModel.fields); - - dataModel.attributes = bases - .flatMap((base) => base.attributes.filter((attr) => filterBaseAttribute(dataModel, base, attr))) - .map((attr) => cloneAst(attr, dataModel, buildReference)) - .concat(dataModel.attributes); - } - - // mark base merged - dataModel.$baseMerged = true; - }); - - // remove abstract models - model.declarations = model.declarations.filter((x) => !(isDataModel(x) && x.isAbstract)); - - model.declarations.filter(isDataModel).forEach((dm) => { - // remove abstract super types - dm.superTypes = dm.superTypes.filter((t) => t.ref && isDelegateModel(t.ref)); - - // fix $containerIndex - linkContentToContainer(dm); - }); - - // fix $containerIndex after deleting abstract models - linkContentToContainer(model); -} - -function filterBaseAttribute(forModel: DataModel, base: DataModel, attr: DataModelAttribute) { - if (attr.$inheritedFrom) { - // don't inherit from skip-level base - return false; - } - - // uninheritable attributes for all inheritance - const uninheritableAttributes = ['@@delegate', '@@map']; - - // uninheritable attributes for delegate inheritance (they reference fields from the base) - const uninheritableFromDelegateAttributes = ['@@unique', '@@index', '@@fulltext']; - - // attributes that are inherited but can be overridden - const overrideAttributes = ['@@schema']; - - if (uninheritableAttributes.includes(attr.decl.$refText)) { - return false; - } - - if ( - // checks if the inheritance is from a delegate model or through one, if so, - // the attribute shouldn't be inherited as the delegate already inherits it - isInheritedFromOrThroughDelegate(forModel, base) && - uninheritableFromDelegateAttributes.includes(attr.decl.$refText) - ) { - return false; - } - - if (hasAttribute(forModel, attr.decl.$refText) && overrideAttributes.includes(attr.decl.$refText)) { - // don't inherit an attribute if it's overridden in the sub model - return false; - } - - return true; -} - -function isInheritedFromOrThroughDelegate(model: DataModel, base: DataModel) { - if (isDelegateModel(base)) { - return true; - } - const chain = getInheritanceChain(model, base); - return !!chain?.some(isDelegateModel); -} - -// deep clone an AST, relink references, and set its container -function cloneAst( - node: T, - newContainer: AstNode, - buildReference: BuildReference -): Mutable { - const clone = copyAstNode(node, buildReference) as Mutable; - clone.$container = newContainer; - - if (isDataModel(newContainer) && isDataModelField(node)) { - // walk up the hierarchy to find the upper-most delegate ancestor that defines the field - const delegateBases = getRecursiveBases(newContainer).filter(isDelegateModel); - clone.$inheritedFrom = delegateBases.findLast((base) => base.fields.some((f) => f.name === node.name)); - } - - if (!clone.$inheritedFrom) { - clone.$inheritedFrom = node.$inheritedFrom ?? getContainerOfType(node, isDataModel); - } - - return clone; -} - -export function isAuthInvocation(node: AstNode) { - return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); -} - -export function isFutureInvocation(node: AstNode) { - return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref); -} - -export function isCheckInvocation(node: AstNode) { - return isInvocationExpr(node) && node.function.ref?.name === 'check' && isFromStdlib(node.function.ref); -} - -export function resolveImportUri(imp: ModelImport): URI | undefined { - if (!imp.path) return undefined; // This will return true if imp.path is undefined, null, or an empty string (""). - - if (!imp.path.endsWith('.zmodel')) { - imp.path += '.zmodel'; - } - - if ( - !imp.path.startsWith('.') && // Respect relative paths - !path.isAbsolute(imp.path) // Respect Absolute paths - ) { - // use the current model's path as the search context - const contextPath = imp.$container.$document - ? path.dirname(imp.$container.$document.uri.fsPath) - : process.cwd(); - imp.path = findNodeModulesFile(imp.path, contextPath) ?? imp.path; - } - - const dirUri = Utils.dirname(getDocument(imp).uri); - return Utils.resolvePath(dirUri, imp.path); -} - -export function resolveTransitiveImports(documents: LangiumDocuments, model: Model): Model[] { - return resolveTransitiveImportsInternal(documents, model); -} - -function resolveTransitiveImportsInternal( - documents: LangiumDocuments, - model: Model, - initialModel = model, - visited: Set = new Set(), - models: Set = new Set() -): Model[] { - const doc = getDocument(model); - const initialDoc = getDocument(initialModel); - - if (initialDoc.uri.fsPath.toLowerCase() !== doc.uri.fsPath.toLowerCase()) { - models.add(model); - } - - const normalizedPath = doc.uri.fsPath.toLowerCase(); - if (!visited.has(normalizedPath)) { - visited.add(normalizedPath); - for (const imp of model.imports) { - const importedModel = resolveImport(documents, imp); - if (importedModel) { - resolveTransitiveImportsInternal(documents, importedModel, initialModel, visited, models); - } - } - } - return Array.from(models); -} - -export function resolveImport(documents: LangiumDocuments, imp: ModelImport): Model | undefined { - const resolvedUri = resolveImportUri(imp); - try { - if (resolvedUri) { - const resolvedDocument = documents.getOrCreateDocument(resolvedUri); - const node = resolvedDocument.parseResult.value; - if (isModel(node)) { - return node; - } - } - } catch { - // NOOP - } - return undefined; -} - -export function getAllDeclarationsIncludingImports(documents: LangiumDocuments, model: Model) { - const imports = resolveTransitiveImports(documents, model); - return model.declarations.concat(...imports.map((imp) => imp.declarations)); -} - -export function getAllDataModelsIncludingImports(documents: LangiumDocuments, model: Model) { - return getAllDeclarationsIncludingImports(documents, model).filter(isDataModel); -} - -export function isCollectionPredicate(node: AstNode): node is BinaryExpr { - return isBinaryExpr(node) && ['?', '!', '^'].includes(node.operator); -} - -export function getContainingDataModel(node: Expression): DataModel | undefined { - let curr: AstNode | undefined = node.$container; - while (curr) { - if (isDataModel(curr)) { - return curr; - } - curr = curr.$container; - } - return undefined; -} - -/** - * Walk upward from the current AST node to find the first node that satisfies the predicate. - */ -export function findUpAst(node: AstNode, predicate: (node: AstNode) => boolean): AstNode | undefined { - let curr: AstNode | undefined = node; - while (curr) { - if (predicate(curr)) { - return curr; - } - curr = curr.$container; - } - return undefined; -} - -/** - * Gets all data models and type defs from all loaded documents - */ -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))) - .toArray(); -} - -/** - * Gets all data models and type defs from loaded and reachable documents - */ -export function getAllLoadedAndReachableDataModelsAndTypeDefs( - langiumDocuments: LangiumDocuments, - fromModel?: DataModel -) { - // get all data models from loaded documents - const allDataModels = getAllLoadedDataModelsAndTypeDefs(langiumDocuments); - - if (fromModel) { - // merge data models transitively reached from the current model - const model = getContainerOfType(fromModel, isModel); - if (model) { - const transitiveDataModels = getAllDataModelsIncludingImports(langiumDocuments, model); - transitiveDataModels.forEach((dm) => { - if (!allDataModels.includes(dm)) { - allDataModels.push(dm); - } - }); - } - } - - return allDataModels; -} - -/** - * 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 { - for (const base of start.superTypes) { - if (base.ref === target) { - return [base.ref]; - } - const path = findUpInheritance(base.ref as DataModel, target); - if (path) { - return [base.ref as DataModel, ...path]; - } - } - return undefined; -} - -/** - * Gets all concrete models that inherit from the given delegate model - */ -export function getConcreteModels(dataModel: DataModel): DataModel[] { - if (!isDelegateModel(dataModel)) { - return []; - } - return dataModel.$container.declarations.filter( - (d): d is DataModel => isDataModel(d) && d !== dataModel && d.superTypes.some((base) => base.ref === dataModel) - ); -} - -/** - * Gets the discriminator field for the given delegate model - */ -export function getDiscriminatorField(dataModel: DataModel) { - const delegateAttr = getAttribute(dataModel, '@@delegate'); - if (!delegateAttr) { - return undefined; - } - const arg = delegateAttr.args[0]?.value; - return isReferenceExpr(arg) ? (arg.target.ref as DataModelField) : undefined; -} diff --git a/packages/schema/src/utils/exec-utils.ts b/packages/schema/src/utils/exec-utils.ts deleted file mode 100644 index 03060a4f9..000000000 --- a/packages/schema/src/utils/exec-utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { execSync as _exec, 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 { - const { env, ...restOptions } = options ?? {}; - const mergedEnv = env ? { ...process.env, ...env } : undefined; - _exec(cmd, { encoding: 'utf-8', stdio: options?.stdio ?? 'inherit', env: mergedEnv, ...restOptions }); -} - -/** - * Utility for running package commands through npx/bunx - */ -export function execPackage(cmd: string, options?: Omit & { env?: Record }): void { - const packageManager = process?.versions?.bun ? 'bunx' : 'npx'; - execSync(`${packageManager} ${cmd}`, options) -} \ No newline at end of file diff --git a/packages/schema/src/utils/is-ci.ts b/packages/schema/src/utils/is-ci.ts deleted file mode 100644 index 4950c331e..000000000 --- a/packages/schema/src/utils/is-ci.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {env} from 'node:process'; -export const isInCi = env.CI !== '0' - && env.CI !== 'false' - && ( - 'CI' in env - || 'CONTINUOUS_INTEGRATION' in env - || Object.keys(env).some(key => key.startsWith('CI_')) - ); \ No newline at end of file diff --git a/packages/schema/src/utils/is-container.ts b/packages/schema/src/utils/is-container.ts deleted file mode 100644 index 23c94a29e..000000000 --- a/packages/schema/src/utils/is-container.ts +++ /dev/null @@ -1,23 +0,0 @@ -import fs from 'node:fs'; -import isDocker from './is-docker'; - -let cachedResult: boolean | undefined; - -// Podman detection -const hasContainerEnv = () => { - try { - fs.statSync('/run/.containerenv'); - return true; - } catch { - return false; - } -}; - -export function isInContainer() { - // TODO: Use `??=` when targeting Node.js 16. - if (cachedResult === undefined) { - cachedResult = hasContainerEnv() || isDocker(); - } - - return cachedResult; -} \ No newline at end of file diff --git a/packages/schema/src/utils/is-docker.ts b/packages/schema/src/utils/is-docker.ts deleted file mode 100644 index c44a12ede..000000000 --- a/packages/schema/src/utils/is-docker.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copied over from https://github.com/sindresorhus/is-docker for CJS compatibility - -import fs from 'node:fs'; - -let isDockerCached: boolean | undefined; - -function hasDockerEnv() { - try { - fs.statSync('/.dockerenv'); - return true; - } catch { - return false; - } -} - -function hasDockerCGroup() { - try { - return fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker'); - } catch { - return false; - } -} - -export default function isDocker() { - // TODO: Use `??=` when targeting Node.js 16. - if (isDockerCached === undefined) { - isDockerCached = hasDockerEnv() || hasDockerCGroup(); - } - - return isDockerCached; -} diff --git a/packages/schema/src/utils/is-wsl.ts b/packages/schema/src/utils/is-wsl.ts deleted file mode 100644 index 564c68076..000000000 --- a/packages/schema/src/utils/is-wsl.ts +++ /dev/null @@ -1,18 +0,0 @@ -import process from 'node:process'; -import os from 'node:os'; -import fs from 'node:fs'; -export const isWsl = () => { - if (process.platform !== 'linux') { - return false; - } - - if (os.release().toLowerCase().includes('microsoft')) { - return true; - } - - try { - return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); - } catch { - return false; - } -}; \ No newline at end of file diff --git a/packages/schema/src/utils/machine-id-utils.ts b/packages/schema/src/utils/machine-id-utils.ts deleted file mode 100644 index 63323a543..000000000 --- a/packages/schema/src/utils/machine-id-utils.ts +++ /dev/null @@ -1,74 +0,0 @@ -// modified from https://github.com/automation-stack/node-machine-id - -import { execSync } from 'child_process'; -import { createHash } from 'crypto'; -import { v4 as uuid } from 'uuid'; - -const { platform } = process; -const win32RegBinPath = { - native: '%windir%\\System32', - mixed: '%windir%\\sysnative\\cmd.exe /c %windir%\\System32', -}; -const guid = { - darwin: 'ioreg -rd1 -c IOPlatformExpertDevice', - win32: - `${win32RegBinPath[isWindowsProcessMixedOrNativeArchitecture()]}\\REG.exe ` + - 'QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography ' + - '/v MachineGuid', - linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname 2> /dev/null) | head -n 1 || :', - freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid', -}; - -function isWindowsProcessMixedOrNativeArchitecture() { - // eslint-disable-next-line no-prototype-builtins - if (process.arch === 'ia32' && process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) { - return 'mixed'; - } - return 'native'; -} - -function hash(guid: string): string { - return createHash('sha256').update(guid).digest('hex'); -} - -function expose(result: string): string { - switch (platform) { - case 'darwin': - return result - .split('IOPlatformUUID')[1] - .split('\n')[0] - .replace(/=|\s+|"/gi, '') - .toLowerCase(); - case 'win32': - return result - .toString() - .split('REG_SZ')[1] - .replace(/\r+|\n+|\s+/gi, '') - .toLowerCase(); - case 'linux': - return result - .toString() - .replace(/\r+|\n+|\s+/gi, '') - .toLowerCase(); - case 'freebsd': - return result - .toString() - .replace(/\r+|\n+|\s+/gi, '') - .toLowerCase(); - default: - throw new Error(`Unsupported platform: ${process.platform}`); - } -} - -export function getMachineId() { - if (!(platform in guid)) { - return uuid(); - } - try { - const value = execSync(guid[platform as keyof typeof guid]); - const id = expose(value.toString()); - return hash(id); - } catch { - return uuid(); - } -} diff --git a/packages/schema/src/utils/pkg-utils.ts b/packages/schema/src/utils/pkg-utils.ts deleted file mode 100644 index e58d1e956..000000000 --- a/packages/schema/src/utils/pkg-utils.ts +++ /dev/null @@ -1,173 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { execSync } from './exec-utils'; -import { match } from 'ts-pattern'; - -export type PackageManagers = 'npm' | 'yarn' | 'pnpm'; - -/** - * A type named FindUp that takes a type parameter e which extends boolean. - * If e extends true, it returns a union type of string[] or undefined. - * If e does not extend true, it returns a union type of string or undefined. - * - * @export - * @template e A type parameter that extends boolean - */ -export type FindUp = e extends true ? string[] | undefined : string | undefined; -/** - * Find and return file paths by searching parent directories based on the given names list and current working directory (cwd) path. - * Optionally return a single path or multiple paths. - * If multiple allowed, return all paths found. - * If no paths are found, return undefined. - * - * @export - * @template [e=false] - * @param names An array of strings representing names to search for within the directory - * @param cwd A string representing the current working directory - * @param [multiple=false as e] A boolean flag indicating whether to search for multiple levels. Useful for finding node_modules directories... - * @param [result=[]] An array of strings representing the accumulated results used in multiple results - * @returns Path(s) to a specific file or folder within the directory or parent directories - */ -export function findUp( - names: string[], - cwd: string = process.cwd(), - multiple: e = false as e, - result: string[] = [] -): FindUp { - if (!names.some((name) => !!name)) return undefined; - const target = names.find((name) => fs.existsSync(path.join(cwd, name))); - if (multiple == false && target) return path.join(cwd, target) as FindUp; - if (target) result.push(path.join(cwd, target)); - const up = path.resolve(cwd, '..'); - if (up === cwd) return (multiple && result.length > 0 ? result : undefined) as FindUp; // it'll fail anyway - return findUp(names, up, multiple, result); -} - -/** - * Find a Node module/file given its name in a specific directory, with a fallback to the current working directory. - * If the name is empty, return undefined. - * Try to resolve the module/file using require.resolve with the specified directory as the starting point. - * Return the resolved path if successful, otherwise return undefined. - * - * @export - * @param {string} name The name of the module/file to find - * @param {string} [cwd=process.cwd()] - * @returns {*} Finds a specified module or file using require.resolve starting from a specified directory path, or the current working directory if not provided. - */ -export function findNodeModulesFile(name: string, cwd: string = process.cwd()) { - if (!name) return undefined; - try { - // 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) { - // If require.resolve fails to find the module/file, it will throw an error. - return undefined; - } -} - -export function getPackageManager(searchStartPath = '.') { - const lockFile = findUp(['yarn.lock', 'bun.lock', 'pnpm-lock.yaml', 'package-lock.json'], searchStartPath); - - if (!lockFile) { - // default use npm - return { packageManager: 'npm', lockFile: undefined, projectRoot: searchStartPath }; - } - - const packageManager = match(path.basename(lockFile)) - .with('yarn.lock', () => 'yarn') - .with('pnpm-lock.yaml', () => 'pnpm') - .with('bun.lock', () => 'bun') - .otherwise(() => 'npm'); - - return { packageManager, lockFile, projectRoot: path.dirname(lockFile) }; -} - -export function installPackage( - pkg: string, - dev: boolean, - pkgManager: PackageManagers | undefined = undefined, - tag = 'latest', - projectPath = '.', - exactVersion = true -) { - const manager = pkgManager ?? getPackageManager(projectPath).packageManager; - console.log(`Installing package "${pkg}@${tag}" with ${manager}`); - switch (manager) { - case 'yarn': - execSync( - `yarn --cwd "${projectPath}" add ${exactVersion ? '--exact' : ''} ${pkg}@${tag} ${dev ? ' --dev' : ''}` - ); - break; - - case 'bun': - execSync( - `bun install --cwd "${projectPath}" ${exactVersion ? '--exact' : ''} ${ - dev ? ' --dev' : '' - } ${pkg}@${tag}` - ); - break; - - case 'pnpm': - execSync( - `pnpm add -C "${projectPath}" ${exactVersion ? '--save-exact' : ''} ${ - dev ? ' --save-dev' : '' - } ${pkg}@${tag}` - ); - break; - - default: - execSync( - `npm install --prefix "${projectPath}" ${exactVersion ? '--save-exact' : ''} ${ - dev ? ' --save-dev' : '' - } ${pkg}@${tag}` - ); - break; - } -} - -export function ensurePackage( - pkg: string, - dev: boolean, - pkgManager: PackageManagers | undefined = undefined, - tag = 'latest', - projectPath = '.', - exactVersion = false -) { - const resolvePath = path.resolve(projectPath); - try { - require.resolve(pkg, { paths: [resolvePath] }); - } catch (err) { - installPackage(pkg, dev, pkgManager, tag, resolvePath, exactVersion); - } -} - -/** - * A function that searches for the nearest package.json file starting from the provided search path or the current working directory if no search path is provided. - * It iterates through the directory structure going one level up at a time until it finds a package.json file. If no package.json file is found, it returns undefined. - * @deprecated Use findUp instead @see findUp - */ -export function findPackageJson(searchPath?: string) { - let currDir = searchPath ?? process.cwd(); - while (currDir) { - const pkgJsonPath = path.join(currDir, 'package.json'); - if (fs.existsSync(pkgJsonPath)) { - return pkgJsonPath; - } - const up = path.resolve(currDir, '..'); - if (up === currDir) { - return undefined; - } - currDir = up; - } - return undefined; -} - -export function getPackageJson(searchPath?: string) { - const pkgJsonPath = findUp(['package.json'], searchPath ?? process.cwd()); - if (pkgJsonPath) { - return JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); - } else { - return undefined; - } -} diff --git a/packages/schema/src/utils/version-utils.ts b/packages/schema/src/utils/version-utils.ts deleted file mode 100644 index 3a2daae57..000000000 --- a/packages/schema/src/utils/version-utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -export function getVersion(): string | undefined { - try { - return require('../package.json').version; - } catch { - try { - // dev environment - return require('../../package.json').version; - } catch { - return undefined; - } - } -} diff --git a/packages/schema/src/vscode/documentation-cache.ts b/packages/schema/src/vscode/documentation-cache.ts deleted file mode 100644 index 6183ee38a..000000000 --- a/packages/schema/src/vscode/documentation-cache.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as vscode from 'vscode'; -import { createHash } from 'crypto'; - -// Cache entry interface -interface CacheEntry { - data: string; - timestamp: number; - extensionVersion: string; -} - -/** - * DocumentationCache class handles persistent caching of ZModel documentation - * using VS Code's globalState for cross-session persistence - */ -export class DocumentationCache implements vscode.Disposable { - private static readonly CACHE_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days cache duration - private static readonly CACHE_PREFIX = 'doc-cache.'; - - private extensionContext: vscode.ExtensionContext; - private extensionVersion: string; - - constructor(context: vscode.ExtensionContext) { - this.extensionContext = context; - this.extensionVersion = context.extension.packageJSON.version as string; - // clear expired cache entries on initialization - this.clearExpiredCache(); - } - - /** - * Dispose of the cache resources (implements vscode.Disposable) - */ - dispose(): void {} - - /** - * Get the cache prefix used for keys - */ - getCachePrefix(): string { - return DocumentationCache.CACHE_PREFIX; - } - - /** - * Enable cache synchronization across machines via VS Code Settings Sync - */ - private enableCacheSync(): void { - const cacheKeys = this.extensionContext.globalState - .keys() - .filter((key) => key.startsWith(DocumentationCache.CACHE_PREFIX)); - if (cacheKeys.length > 0) { - this.extensionContext.globalState.setKeysForSync(cacheKeys); - } - } - - /** - * Generate a cache key from request body with normalized content - */ - private generateCacheKey(models: string[]): string { - // Remove ALL whitespace characters from each model string for cache key generation - // This ensures identical content with different formatting uses the same cache - const normalizedModels = models.map((model) => model.replace(/\s/g, '')).sort(); - const hash = createHash('sha512') - .update(JSON.stringify({ models: normalizedModels })) - .digest('hex'); - return `${DocumentationCache.CACHE_PREFIX}${hash}`; - } - - /** - * Check if cache entry is still valid (not expired) - */ - private isCacheValid(entry: CacheEntry): boolean { - return Date.now() - entry.timestamp < DocumentationCache.CACHE_DURATION_MS; - } - - /** - * Get cached response if available and valid - */ - async getCachedResponse(models: string[]): Promise { - const cacheKey = this.generateCacheKey(models); - const entry = this.extensionContext.globalState.get(cacheKey); - - if (entry && this.isCacheValid(entry)) { - console.log('Using cached documentation response from persistent storage'); - return entry.data; - } - - // Clean up expired entry if it exists - if (entry) { - await this.extensionContext.globalState.update(cacheKey, undefined); - } - - return null; - } - - /** - * Cache a response for future use - */ - async setCachedResponse(models: string[], data: string): Promise { - const cacheKey = this.generateCacheKey(models); - const cacheEntry: CacheEntry = { - data, - timestamp: Date.now(), - extensionVersion: this.extensionVersion, - }; - - await this.extensionContext.globalState.update(cacheKey, cacheEntry); - - // Update sync keys to include new cache entry - this.enableCacheSync(); - } - - /** - * Clear expired cache entries from persistent storage - */ - async clearExpiredCache(): Promise { - const now = Date.now(); - let clearedCount = 0; - const allKeys = this.extensionContext.globalState.keys(); - - for (const key of allKeys) { - if (key.startsWith(DocumentationCache.CACHE_PREFIX)) { - const entry = this.extensionContext.globalState.get(key); - if ( - entry?.extensionVersion !== this.extensionVersion || - now - entry.timestamp >= DocumentationCache.CACHE_DURATION_MS - ) { - await this.extensionContext.globalState.update(key, undefined); - clearedCount++; - } - } - } - - if (clearedCount > 0) { - console.log(`Cleared ${clearedCount} expired cache entries from persistent storage`); - } - } - - /** - * Clear all cache entries from persistent storage - */ - async clearAllCache(): Promise { - const allKeys = this.extensionContext.globalState.keys(); - let clearedCount = 0; - - for (const key of allKeys) { - if (key.startsWith(DocumentationCache.CACHE_PREFIX)) { - await this.extensionContext.globalState.update(key, undefined); - clearedCount++; - } - } - - console.log(`Cleared all cache entries from persistent storage (${clearedCount} items)`); - } -} diff --git a/packages/schema/src/vscode/release-notes-manager.ts b/packages/schema/src/vscode/release-notes-manager.ts deleted file mode 100644 index 0c44cba72..000000000 --- a/packages/schema/src/vscode/release-notes-manager.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as vscode from 'vscode'; - -/** - * ReleaseNotesManager class handles release notes functionality - */ -export class ReleaseNotesManager implements vscode.Disposable { - private extensionContext: vscode.ExtensionContext; - private readonly zmodelPreviewReleaseNoteKey = 'zmodel-preview-release-note-shown'; - - constructor(context: vscode.ExtensionContext) { - this.extensionContext = context; - this.initialize(); - } - - /** - * Initialize and register commands, show release notes if first time - */ - initialize(): void { - this.showReleaseNotesIfFirstTime(); - } - - /** - * Show release notes on first activation of this version - */ - async showReleaseNotesIfFirstTime(): Promise { - // Show release notes if this is the first time activating this version - if (!this.extensionContext.globalState.get(this.zmodelPreviewReleaseNoteKey)) { - await this.showReleaseNotes(); - // Update the stored version to prevent showing again - await this.extensionContext.globalState.update(this.zmodelPreviewReleaseNoteKey, true); - // Add this key to sync keys for cross-machine synchronization - this.extensionContext.globalState.setKeysForSync([this.zmodelPreviewReleaseNoteKey]); - } - } - - /** - * Show release notes (can be called manually) - */ - async showReleaseNotes(): Promise { - try { - // Read the release notes HTML file - const releaseNotesPath = vscode.Uri.joinPath( - this.extensionContext.extensionUri, - 'bundle/res/zmodel-preview-release-notes.html' - ); - - const htmlBytes = await vscode.workspace.fs.readFile(releaseNotesPath); - const htmlContent = Buffer.from(htmlBytes).toString('utf8'); - // Create and show the release notes webview - const panel = vscode.window.createWebviewPanel( - 'ZenstackReleaseNotes', - 'ZenStack - New Feature Announcement!', - vscode.ViewColumn.One, - { - enableScripts: true, - retainContextWhenHidden: true, - } - ); - - panel.webview.html = htmlContent; - - // Optional: Close the panel when user clicks outside or after some time - panel.onDidDispose(() => { - // Panel disposed - }); - } catch (error) { - console.error('Error showing release notes:', error); - } - } - - /** - * Dispose of resources - */ - dispose(): void { - // Any cleanup if needed - } -} diff --git a/packages/schema/src/vscode/res/zmodel-preview-release-notes.html b/packages/schema/src/vscode/res/zmodel-preview-release-notes.html deleted file mode 100644 index 262895d87..000000000 --- a/packages/schema/src/vscode/res/zmodel-preview-release-notes.html +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - -
-

🎉 Introducing ZModel Documentation Preview

-

Preview documentation directly from your ZModel powered by AI

-
- -
-

📖 What's New

-

- You can now preview comprehensive documentation for your ZModel files, just like you would preview a - markdown file. -

-
- -
-

🚀 How to Use

-
    -
  1. Open your .zmodel file
  2. -
  3. - Click () in the editor toolbar, or press - Cmd + Shift + V (Mac) or - Ctrl + Shift + V (Windows) -
  4. -
  5. Sign in with ZenStack (one-time setup)
  6. -
  7. - Click () in the preview toolbar to save the doc, or press - Cmd + Shift + S (Mac) or - Ctrl + Shift + S (Windows) -
  8. -
-
- -
-

💡 Tips

-
    -
  • Ensure your zmodel is error-free before generating.
  • -
  • Use your main zmodel file, which will include all imported models, for complete documentation.
  • -
  • - Add clear, descriptive comments in your ZModel. The more context you provide, the better the - results. -
  • -
-
- -

- Happy coding with ZenStack! 🚀
-

- - diff --git a/packages/schema/src/vscode/vscode-telemetry.ts b/packages/schema/src/vscode/vscode-telemetry.ts deleted file mode 100644 index 733733ee0..000000000 --- a/packages/schema/src/vscode/vscode-telemetry.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { init, Mixpanel } from 'mixpanel'; -import * as os from 'os'; -import * as vscode from 'vscode'; -import { getMachineId } from '../utils/machine-id-utils'; -import { v5 as uuidv5 } from 'uuid'; - -export const VSCODE_TELEMETRY_TRACKING_TOKEN = ''; - -export type TelemetryEvents = - | 'extension:activate' - | 'extension:zmodel-preview' - | 'extension:zmodel-save' - | 'extension:signin:show' - | 'extension:signin:start' - | 'extension:signin:error' - | 'extension:signin:complete'; - -export class VSCodeTelemetry { - private readonly mixpanel: Mixpanel | undefined; - private readonly deviceId = this.getDeviceId(); - private readonly _os_type = os.type(); - private readonly _os_release = os.release(); - private readonly _os_arch = os.arch(); - private readonly _os_version = os.version(); - private readonly _os_platform = os.platform(); - private readonly vscodeAppName = vscode.env.appName; - private readonly vscodeVersion = vscode.version; - private readonly vscodeAppHost = vscode.env.appHost; - - constructor() { - if (vscode.env.isTelemetryEnabled) { - this.mixpanel = init(VSCODE_TELEMETRY_TRACKING_TOKEN, { - geolocate: true, - }); - } - } - - private getDeviceId() { - const hostId = getMachineId(); - // namespace UUID for generating UUIDv5 from DNS 'zenstack.dev' - return uuidv5(hostId, '133cac15-3efb-50fa-b5fc-4b90e441e563'); - } - - track(event: TelemetryEvents, properties: Record = {}) { - if (this.mixpanel) { - const payload = { - distinct_id: this.deviceId, - time: new Date(), - $os: this._os_type, - osType: this._os_type, - osRelease: this._os_release, - osPlatform: this._os_platform, - osArch: this._os_arch, - osVersion: this._os_version, - nodeVersion: process.version, - vscodeAppName: this.vscodeAppName, - vscodeVersion: this.vscodeVersion, - vscodeAppHost: this.vscodeAppHost, - ...properties, - }; - this.mixpanel.track(event, payload); - } - } - - identify(userId: string) { - if (this.mixpanel) { - this.mixpanel.track('$identify', { - $identified_id: userId, - $anon_id: this.deviceId, - token: VSCODE_TELEMETRY_TRACKING_TOKEN, - }); - } - } -} -export default new VSCodeTelemetry(); diff --git a/packages/schema/src/vscode/zenstack-auth-provider.ts b/packages/schema/src/vscode/zenstack-auth-provider.ts deleted file mode 100644 index ba7c32a0b..000000000 --- a/packages/schema/src/vscode/zenstack-auth-provider.ts +++ /dev/null @@ -1,255 +0,0 @@ -import * as vscode from 'vscode'; -import telemetry from './vscode-telemetry'; -interface JWTClaims { - jti?: string; - sub?: string; - email?: string; - exp?: number; - [key: string]: unknown; -} - -export const AUTH_PROVIDER_ID = 'ZenStack'; -export const AUTH_URL = 'https://accounts.zenstack.dev'; -export const API_URL = 'https://api.zenstack.dev'; - -export class ZenStackAuthenticationProvider implements vscode.AuthenticationProvider, vscode.Disposable { - private _onDidChangeSessions = - new vscode.EventEmitter(); - public readonly onDidChangeSessions = this._onDidChangeSessions.event; - - private _sessions: vscode.AuthenticationSession[] = []; - private _context: vscode.ExtensionContext; - private _disposable: vscode.Disposable; - private pendingAuth?: { - resolve: (session: vscode.AuthenticationSession) => void; - reject: (error: Error) => void; - scopes: readonly string[]; - }; - - constructor(context: vscode.ExtensionContext) { - this._context = context; - - this._disposable = vscode.Disposable.from( - vscode.authentication.registerAuthenticationProvider(AUTH_PROVIDER_ID, 'ZenStack', this), - vscode.window.registerUriHandler({ - handleUri: async (uri: vscode.Uri) => { - if (uri.path === '/auth-callback') { - await this.handleAuthCallback(uri); - } - }, - }), - // Register logout command - vscode.commands.registerCommand('zenstack.logout', async () => { - await this.logoutAllSessions(); - }) - ); - } - - async getSessions(_scopes?: readonly string[]): Promise { - // Check if we have stored sessions in VS Code's secret storage - const storedSessions = await this.getStoredSessions(); - this._sessions = storedSessions; - return this._sessions; - } - - async createSession(scopes: readonly string[]): Promise { - // Create a login flow - const session = await this.performLogin(scopes); - if (session) { - this._sessions.push(session); - await this.storeSession(session); - this._onDidChangeSessions.fire({ - added: [session], - removed: [], - changed: [], - }); - } - return session; - } - - async removeSession(sessionId: string): Promise { - const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId); - if (sessionIndex > -1) { - const session = this._sessions[sessionIndex]; - this._sessions.splice(sessionIndex, 1); - await this.removeStoredSession(sessionId); - this._onDidChangeSessions.fire({ - added: [], - removed: [session], - changed: [], - }); - } - } - - /** - * Log out all sessions - */ - async logoutAllSessions(): Promise { - if (this._sessions.length === 0) { - return; - } - - (await this.getSessions()).forEach(async (s) => await this.removeSession(s.id)); - vscode.window.showInformationMessage('Successfully logged out of ZenStack.'); - } - - private async performLogin(scopes: readonly string[]): Promise { - // Create the authentication promise - return vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: 'Signing in to ZenStack', - cancellable: true, - }, - async (progress, token) => { - return new Promise((resolve, reject) => { - // Handle cancellation - token.onCancellationRequested(() => { - if (this.pendingAuth) { - delete this.pendingAuth; - } - reject(new Error('User Cancelled')); - }); - - const isCursor = vscode.env.appName == 'Cursor'; - - let signInUrl = vscode.Uri.parse(new URL('/sign-in', AUTH_URL).toString()); - - if (isCursor) { - signInUrl = signInUrl.with({ - query: `redirect_url=${API_URL}/oauth/oauth_callback?vscodeapp=cursor`, - }); - } - - console.log('ZenStack sign-in URL:', signInUrl.toString()); - // Store the state and resolve function for later use - this.pendingAuth = { resolve, reject, scopes }; - - // Open the ZenStack sign-in page in the user's default browser - vscode.env.openExternal(signInUrl).then( - () => { - console.log('Opened ZenStack sign-in page in browser'); - progress.report({ message: 'Waiting for return from browser...' }); - }, - (error) => { - if (this.pendingAuth) { - delete this.pendingAuth; - } - reject(new Error(`Failed to open sign-in page: ${error}`)); - } - ); - - // 2 minutes timeout - setTimeout(() => { - if (this.pendingAuth) { - delete this.pendingAuth; - } - reject(new Error('Timeout')); - }, 120000); - }); - } - ); - } - - // Handle authentication callback from ZenStack - public async handleAuthCallback(callbackUri: vscode.Uri): Promise { - const query = new URLSearchParams(callbackUri.query); - const accessToken = query.get('access_token'); - if (!this.pendingAuth) { - console.warn('No pending authentication found'); - return; - } - if (!accessToken) { - this.pendingAuth.reject(new Error('No access token received')); - delete this.pendingAuth; - return; - } - try { - // Create session from the access token - const session = await this.createSessionFromAccessToken(accessToken); - this.pendingAuth.resolve(session); - delete this.pendingAuth; - } catch (error) { - if (this.pendingAuth) { - this.pendingAuth.reject(error instanceof Error ? error : new Error(String(error))); - delete this.pendingAuth; - } - } - } - - private async createSessionFromAccessToken(accessToken: string): Promise { - try { - // Decode JWT to get claims - const claims = this.parseJWTClaims(accessToken); - telemetry.identify(claims.email!); - return { - id: claims.jti || Math.random().toString(36), - accessToken: accessToken, - account: { - id: claims.sub || 'unknown', - label: claims.email || 'unknown@zenstack.dev', - }, - scopes: [], - }; - } catch (error) { - throw new Error(`Failed to create session from access token: ${error}`); - } - } - - private parseJWTClaims(token: string): JWTClaims { - try { - // JWT tokens have 3 parts separated by dots: header.payload.signature - const parts = token.split('.'); - if (parts.length !== 3) { - throw new Error('Invalid JWT format'); - } - - // Decode the payload (second part) - const payload = parts[1]; - // Add padding if needed for base64 decoding - const paddedPayload = payload + '='.repeat((4 - (payload.length % 4)) % 4); - const decoded = atob(paddedPayload); - - return JSON.parse(decoded); - } catch (error) { - throw new Error(`Failed to parse JWT claims: ${error}`); - } - } - - private async getStoredSessions(): Promise { - try { - const stored = await this._context.secrets.get('zenstack-auth-sessions'); - return stored ? JSON.parse(stored) : []; - } catch (error) { - console.error('Error retrieving stored sessions:', error); - return []; - } - } - - private async storeSession(session: vscode.AuthenticationSession): Promise { - try { - const sessions = await this.getStoredSessions(); - sessions.push(session); - await this._context.secrets.store('zenstack-auth-sessions', JSON.stringify(sessions)); - } catch (error) { - console.error('Error storing session:', error); - } - } - - private async removeStoredSession(sessionId: string): Promise { - try { - const sessions = await this.getStoredSessions(); - const filteredSessions = sessions.filter((s) => s.id !== sessionId); - await this._context.secrets.store('zenstack-auth-sessions', JSON.stringify(filteredSessions)); - } catch (error) { - console.error('Error removing stored session:', error); - } - } - - /** - * Dispose the registered services - */ - public async dispose() { - this._disposable.dispose(); - } -} diff --git a/packages/schema/src/vscode/zmodel-preview.ts b/packages/schema/src/vscode/zmodel-preview.ts deleted file mode 100644 index 9826bd980..000000000 --- a/packages/schema/src/vscode/zmodel-preview.ts +++ /dev/null @@ -1,377 +0,0 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as os from 'os'; -import { z } from 'zod'; -import { LanguageClient } from 'vscode-languageclient/node'; -import { URI } from 'vscode-uri'; -import { DocumentationCache } from './documentation-cache'; -import { requireAuth } from '../extension'; -import { API_URL } from './zenstack-auth-provider'; -import telemetry from './vscode-telemetry'; - -/** - * ZModelPreview class handles ZModel file preview functionality - */ -export class ZModelPreview implements vscode.Disposable { - private documentationCache: DocumentationCache; - private languageClient: LanguageClient; - private lastGeneratedMarkdown: string | null = null; - // use a zero-width space in the file name to make it non-colliding with user file - private readonly previewZModelFileName = `zmodel${'\u200B'}-preview.md`; - - // Schema for validating the request body - private static DocRequestSchema = z.object({ - models: z.array( - z.object({ - path: z.string().optional(), - content: z.string(), - }) - ), - environments: z - .object({ - vscodeAppName: z.string(), - vscodeVersion: z.string(), - vscodeAppHost: z.string(), - osRelease: z.string(), - osType: z.string(), - }) - .optional(), - }); - - constructor(context: vscode.ExtensionContext, client: LanguageClient, cache: DocumentationCache) { - this.documentationCache = cache; - this.languageClient = client; - this.initialize(context); - } - - /** - * Initialize and register commands - */ - initialize(context: vscode.ExtensionContext): void { - this.registerCommands(context); - - context.subscriptions.push( - vscode.window.tabGroups.onDidChangeTabs(() => { - const activeTabLabels = vscode.window.tabGroups.all.filter((group) => - group.activeTab?.label?.endsWith(this.previewZModelFileName) - ); - if (activeTabLabels.length > 0) { - vscode.commands.executeCommand('setContext', 'zenstack.isMarkdownPreview', true); - } else { - vscode.commands.executeCommand('setContext', 'zenstack.isMarkdownPreview', false); - } - }) - ); - } - - /** - * Register ZModel preview commands - */ - private registerCommands(context: vscode.ExtensionContext): void { - // Register the preview command for zmodel files - context.subscriptions.push( - vscode.commands.registerCommand('zenstack.preview-zmodel', async () => { - await this.previewZModelFile(); - }) - ); - - // Register the save documentation command for zmodel files - context.subscriptions.push( - vscode.commands.registerCommand('zenstack.save-zmodel-documentation', async () => { - await this.saveZModelDocumentation(); - }) - ); - - // Register cache management commands - context.subscriptions.push( - vscode.commands.registerCommand('zenstack.clear-documentation-cache', async () => { - await this.documentationCache.clearAllCache(); - vscode.window.showInformationMessage('ZenStack documentation cache cleared'); - }) - ); - } - - /** - * Preview a ZModel file - */ - async previewZModelFile(): Promise { - telemetry.track('extension:zmodel-preview'); - const editor = vscode.window.activeTextEditor; - - if (!editor) { - vscode.window.showErrorMessage('No active editor found.'); - return; - } - - const document = editor.document; - if (!document.fileName.endsWith('.zmodel')) { - vscode.window.showErrorMessage('The active file is not a ZModel file.'); - return; - } - - // Check authentication before proceeding - const session = await requireAuth(); - if (!session) { - return; - } - - try { - this.checkForMermaidExtensions(); - // Show progress indicator - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: 'Generating ZModel documentation...', - cancellable: false, - }, - async () => { - const markdownContent = await this.generateZModelDocumentation(document); - - if (markdownContent) { - // Store the generated content for potential saving later - this.lastGeneratedMarkdown = markdownContent; - - await this.openMarkdownPreview(markdownContent); - } - } - ); - } catch (error) { - console.error('Error previewing ZModel:', error); - vscode.window.showErrorMessage( - `Failed to preview ZModel: ${error instanceof Error ? error.message : String(error)}` - ); - } - } - - /** - * Get all imported ZModel URIs using the language server - */ - private async getAllImportedZModelURIs(document: vscode.TextDocument): Promise<{ - hasSyntaxErrors: boolean; - importedURIs: URI[]; - }> { - if (!this.languageClient) { - throw new Error('Language client not initialized'); - } - - try { - // Ensure the language server is ready - await this.languageClient.start(); - - // Send the custom request to get all imported ZModel URIs - const result = await this.languageClient.sendRequest('zenstack/getAllImportedZModelURIs', { - textDocument: { - uri: document.uri.toString(), - }, - }); - - return result as { - hasSyntaxErrors: boolean; - importedURIs: URI[]; - }; - } catch (error) { - console.error('Error getting AST from language server:', error); - throw error; - } - } - - /** - * Generate documentation for ZModel - */ - private async generateZModelDocumentation(document: vscode.TextDocument): Promise { - try { - const astInfo = await this.getAllImportedZModelURIs(document); - - if (astInfo?.hasSyntaxErrors !== false) { - vscode.window.showWarningMessage('Please fix the errors in the ZModel first'); - return ''; - } - - const importedURIs = astInfo?.importedURIs; - - // get vscode document from importedURIs - const importedModels = await Promise.all( - importedURIs.map(async (uri) => { - try { - const fileUri = vscode.Uri.file(uri.path); - const fileContent = await vscode.workspace.fs.readFile(fileUri); - const filePath = fileUri.path; - return { content: Buffer.from(fileContent).toString('utf8').trim(), path: filePath }; - } catch (error) { - throw new Error( - `Failed to read imported ZModel file at ${uri.path}: ${ - error instanceof Error ? error.message : String(error) - }` - ); - } - }) - ); - - const allModels = [{ content: document.getText().trim(), path: document.uri.path }, ...importedModels]; - - const session = await requireAuth(); - if (!session) { - throw new Error('Authentication required to generate documentation'); - } - - // Prepare request body - const requestBody: z.infer = { - models: allModels, - environments: { - vscodeAppName: vscode.env.appName, - vscodeVersion: vscode.version, - vscodeAppHost: vscode.env.appHost, - osRelease: os.release(), - osType: os.type(), - }, - }; - - const allModelsContent = allModels.map((m) => m.content); - - // Check cache first - const cachedResponse = await this.documentationCache.getCachedResponse(allModelsContent); - if (cachedResponse) { - return cachedResponse; - } - - // record the time spent - const startTime = Date.now(); - const apiResponse = await fetch(`${API_URL}/api/doc`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - authorization: session.accessToken, - }, - body: JSON.stringify(requestBody), - }); - - console.log(`API request completed in ${Date.now() - startTime} ms`); - - if (!apiResponse.ok) { - throw new Error(`API request failed: ${apiResponse.status} ${apiResponse.statusText}`); - } - - const responseText = await apiResponse.text(); - - // Cache the response - await this.documentationCache.setCachedResponse(allModelsContent, responseText); - - return responseText; - } catch (error) { - console.error('Error generating documentation:', error); - const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to generate documentation: ${errorMessage}`); - } - } - - /** - * Open markdown preview - */ - private async openMarkdownPreview(markdownContent: string): Promise { - // Create a temporary markdown file with a descriptive name in the system temp folder - const tempFilePath = path.join(os.tmpdir(), this.previewZModelFileName); - const tempFile = vscode.Uri.file(tempFilePath); - - try { - // Write the markdown content to the temp file - await vscode.workspace.fs.writeFile(tempFile, new TextEncoder().encode(markdownContent)); - // Open the markdown preview side by side - await vscode.commands.executeCommand('markdown.showPreviewToSide', tempFile); - } catch (error) { - console.error('Error creating markdown preview:', error); - throw new Error( - `Failed to create markdown preview: ${error instanceof Error ? error.message : String(error)}` - ); - } - } - - /** - * Save ZModel documentation to a user-selected file - */ - async saveZModelDocumentation(): Promise { - telemetry.track('extension:zmodel-save'); - // Check if we have cached content first - if (!this.lastGeneratedMarkdown) { - vscode.window.showErrorMessage( - 'No documentation content available to save. Please generate the documentation first by running "Preview ZModel Documentation".' - ); - return; - } - - // Show save dialog - let defaultFilePath = `zmodel-doc.md`; - - const workspaceFolders = vscode.workspace.workspaceFolders; - if (workspaceFolders && workspaceFolders.length > 0) { - const workspacePath = workspaceFolders[0].uri.fsPath; - // If the workspace folder exists, use it - defaultFilePath = path.join(workspacePath, defaultFilePath); - } - - const saveUri = await vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file(defaultFilePath), - filters: { - Markdown: ['md'], - 'All Files': ['*'], - }, - saveLabel: 'Save Documentation', - }); - - if (!saveUri) { - return; // User cancelled - } - - try { - // Write the markdown content to the selected file - await vscode.workspace.fs.writeFile(saveUri, new TextEncoder().encode(this.lastGeneratedMarkdown)); - // Open and close the saved file to refresh the shown markdown preview - await vscode.commands.executeCommand('vscode.open', saveUri); - await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); - } catch (error) { - console.error('Error saving markdown file:', error); - vscode.window.showErrorMessage( - `Failed to save documentation: ${error instanceof Error ? error.message : String(error)}` - ); - } - } - - /** - * Check for Mermaid extensions - */ - private checkForMermaidExtensions(): void { - const setting = vscode.workspace.getConfiguration('zenstack').get('searchForExtensions'); - if (setting !== false) { - const extensions = vscode.extensions.all.filter((extension) => - ['markdown-mermaid', 'vscode-mermaid-chart', 'vscode-mermaid-preview'].some((name) => - extension.packageJSON.name?.toLowerCase().includes(name.toLowerCase()) - ) - ); - if (extensions.length === 0) { - const searchAction = 'Search'; - const stopShowing = "Don't show again"; - vscode.window - .showInformationMessage( - 'Search for extensions to view mermaid chart in ZModel preview doc?', - searchAction, - stopShowing - ) - .then((selectedAction) => { - if (selectedAction === searchAction) { - vscode.commands.executeCommand('workbench.extensions.search', 'markdown-mermaid'); - } else if (selectedAction === stopShowing) { - vscode.workspace - .getConfiguration('zenstack') - .update('searchForExtensions', false, vscode.ConfigurationTarget.Global); - } - }); - } - } - } - - /** - * Dispose of resources - */ - dispose(): void { - // Any cleanup if needed - } -} diff --git a/packages/schema/tests/generator/expression-writer.test.ts b/packages/schema/tests/generator/expression-writer.test.ts deleted file mode 100644 index af7cf576c..000000000 --- a/packages/schema/tests/generator/expression-writer.test.ts +++ /dev/null @@ -1,1396 +0,0 @@ -/// - -import { DataModel, Enum, Expression, isDataModel, isEnum } from '@zenstackhq/language/ast'; -import * as tmp from 'tmp'; -import { Project, VariableDeclarationKind } from 'ts-morph'; -import { ExpressionWriter } from '../../src/plugins/enhancer/policy/expression-writer'; -import { loadModel } from '../utils'; - -tmp.setGracefulCleanup(); - -describe('Expression Writer Tests', () => { - it('boolean literal', async () => { - await check( - ` - model Test { - id String @id - @@allow('all', true) - } - `, - (model) => model.attributes[0].args[1].value, - `{ AND: [] }` - ); - - await check( - ` - model Test { - id String @id - @@allow('all', false) - } - `, - (model) => model.attributes[0].args[1].value, - `{ OR: [] }` - ); - }); - - it('boolean field', async () => { - await check( - ` - model Test { - id String @id - flag Boolean - @@allow('all', flag) - } - `, - (model) => model.attributes[0].args[1].value, - `{ flag: true }` - ); - - await check( - ` - model Test { - id String @id - flag Boolean - @@allow('all', !flag) - } - `, - (model) => model.attributes[0].args[1].value, - `{ NOT: { flag: true } }` - ); - }); - - it('enum', async () => { - await check( - ` - enum Role { - USER - ADMIN - } - - model Test { - id String @id - role Role - @@allow('all', role == ADMIN) - } - `, - (model) => model.attributes[0].args[1].value, - `{ role: { equals: Role.ADMIN } }` - ); - }); - - it('field against literal', async () => { - await check( - ` - model Test { - id String @id - x Int - @@allow('all', x > 0) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - x: { - gt: 0 - } - }` - ); - - await check( - ` - model Test { - id String @id - label String - @@allow('all', label == 'thing') - } - `, - (model) => model.attributes[0].args[1].value, - `{ - label: { - equals: 'thing' - } - }` - ); - }); - - it('this reference', async () => { - await check( - ` - model Test { - id String @id - @@allow('all', this == this) - } - `, - (model) => model.attributes[0].args[1].value, - `{OR:[]}` - ); - - await check( - ` - model Test { - id String @id - x Int - @@allow('all', this.x > 0) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - x: { - gt: 0 - } - }` - ); - }); - - it('logical', async () => { - await check( - ` - model Test { - id String @id - x Int - @@allow('all', x > 0 && x > 1) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - AND: - [ - { - x : { - gt: 0 - } - } - , - { - x : { - gt: 1 - } - } - ] - }` - ); - - await check( - ` - model Test { - id String @id - x Int - @@allow('all', x > 0 || x > 1) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - OR: - [ - { - x : { - gt: 0 - } - } - , - { - x : { - gt: 1 - } - } - ] - }` - ); - - await check( - ` - model Test { - id String @id - x Int - @@allow('all', x > 0 && x > 1 || x > 2) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - OR: - [ - { - AND: - [ - { - x : { - gt: 0 - } - } - , - { - x : { - gt: 1 - } - } - ] - } - , - { - x : { - gt: 2 - } - } - ] - }` - ); - - await check( - ` - model Test { - id String @id - x Int - @@allow('all', !(x > 0 && x > 1 || !(x > 2))) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - NOT: - { - OR: - [ - { - AND: - [ - { - x : { - gt: 0 - } - } - , - { - x : { - gt: 1 - } - } - ] - } - , - { - NOT: - { - x : { - gt: 2 - } - } - } - ] - } - }` - ); - }); - - it('to-one relation query', async () => { - await check( - ` - model Foo { - id String @id - x Int - t Test? - } - - model Test { - id String @id - foo Foo @relation(fields: [fooId], references: [id]) - fooId String @unique - @@deny('all', foo.x <= 0) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - foo: { - x : { - lte: 0 - } - } - }` - ); - - await check( - ` - model Foo { - id String @id - t Test? - x Int - } - - model Test { - id String @id - foo Foo @relation(fields: [fooId], references: [id]) - fooId String @unique - @@deny('all', !(foo.x > 0)) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - NOT: - { - foo: { - x : { - gt: 0 - } - } - } - }` - ); - - await check( - ` - model Foo { - id String @id - t Test? - x Boolean - } - - model Test { - id String @id - foo Foo @relation(fields: [fooId], references: [id]) - fooId String @unique - @@deny('all', !foo.x) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - NOT: { - foo: { - x: true - } - } - }` - ); - - await check( - ` - model Foo { - id String @id - bar Bar? - t Test? - } - - model Bar { - id String @id - x Int - foo Foo @relation(fields: [fooId], references: [id]) - fooId String @unique - } - - model Test { - id String @id - foo Foo @relation(fields: [fooId], references: [id]) - fooId String @unique - @@deny('all', foo.bar.x <= 0) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - foo: { - bar: { - x : { - lte: 0 - } - } - } - }` - ); - }); - - it('to-many relation query', async () => { - await check( - ` - model Foo { - id String @id - t Test @relation(fields: [tId], references: [id]) - tId String - x Int - } - - model Test { - id String @id - foos Foo[] - @@deny('all', foos?[x <= 0]) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - foos: { - some: { - x: { - lte: 0 - } - } - } - }` - ); - - await check( - ` - model Foo { - id String @id - t Test @relation(fields: [tId], references: [id]) - tId String - x Int - } - - model Test { - id String @id - foos Foo[] - @@deny('all', foos![x <= 0]) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - foos: { - every: { - x: { - lte: 0 - } - } - } - }` - ); - - await check( - ` - model Foo { - id String @id - t Test @relation(fields: [tId], references: [id]) - tId String - x Int - } - - model Test { - id String @id - foos Foo[] - @@deny('all', foos^[x <= 0]) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - foos: { - none: { - x: { - lte: 0 - } - } - } - }` - ); - - await check( - ` - model Foo { - id String @id - bars Bar[] - t Test @relation(fields: [tId], references: [id]) - tId String @unique - } - - model Bar { - id String @id - foo Foo @relation(fields: [fooId], references: [id]) - fooId String - x Int - } - - model Test { - id String @id - foo Foo? - @@deny('all', foo.bars?[x <= 0]) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - foo: { - bars: { - some: { - x: { - lte: 0 - } - } - } - } - }` - ); - }); - - it('auth null check', async () => { - await check( - ` - model User { - id String @id - } - - model Test { - id String @id - @@allow('all', auth() == null) - } - `, - (model) => model.attributes[0].args[1].value, - `(user==null)?{AND:[]}:{OR:[]}`, - '{ id: "1" }' - ); - - await check( - ` - model User { - x String - y String - @@id([x, y]) - } - - model Test { - id String @id - @@allow('all', auth() == null) - } - `, - (model) => model.attributes[0].args[1].value, - `(user==null)?{AND:[]}:{OR:[]}`, - '{ x: "1", y: "2" }' - ); - - await check( - ` - model User { - id String @id - } - - model Test { - id String @id - @@allow('all', auth() != null) - } - `, - (model) => model.attributes[0].args[1].value, - `(user!=null)?{AND:[]}:{OR:[]}`, - '{ id: "1" }' - ); - - await check( - ` - model User { - x String - y String - @@id([x, y]) - } - - model Test { - id String @id - @@allow('all', auth() != null) - } - `, - (model) => model.attributes[0].args[1].value, - `(user!=null)?{AND:[]}:{OR:[]}`, - '{ x: "1", y: "2" }' - ); - }); - - it('auth boolean field check', async () => { - await check( - ` - model User { - id String @id - admin Boolean - } - - model Test { - id String @id - @@allow('all', auth().admin) - } - `, - (model) => model.attributes[0].args[1].value, - `!!(user?.admin??null)?{AND:[]}:{OR:[]}`, - '{ id: "1", admin: true }' - ); - - await check( - ` - model User { - id String @id - admin Boolean - } - - model Test { - id String @id - @@deny('all', !auth().admin) - } - `, - (model) => model.attributes[0].args[1].value, - `{ NOT: !!(user?.admin??null)?{AND:[]}:{OR:[]} }`, - '{ id: "1", admin: true }' - ); - }); - - it('auth check against field single id', async () => { - await check( - ` - model User { - id String @id - t Test? - } - - model Test { - id String @id - owner User @relation(fields: [ownerId], references: [id]) - ownerId String @unique - @@allow('all', auth() == owner) - } - `, - (model) => model.attributes[0].args[1].value, - `(user==null) ? { OR: [] } : { owner: { is: { id : user.id } } }` - ); - - await check( - ` - model User { - id String @id - t Test? - } - - model Test { - id String @id - owner User @relation(fields: [ownerId], references: [id]) - ownerId String @unique - @@deny('all', auth() != owner) - } - `, - (model) => model.attributes[0].args[1].value, - `(user==null) ? { AND: [] } : - { - owner: { - isNot: { id: user.id } - } - }` - ); - - await check( - ` - model User { - id String @id - t Test? - } - - model Test { - id String @id - owner User @relation(fields: [ownerId], references: [id]) - ownerId String @unique - @@allow('all', auth().id == owner.id) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.id??null)==null) ? - { OR: [] } : - { owner: { id: { equals: (user?.id ?? null) } } }` - ); - }); - - it('auth check against field multi-id', async () => { - await check( - ` - model User { - x String - y String - t Test? - @@id([x, y]) - } - - model Test { - id String @id - owner User @relation(fields: [ownerX, ownerY], references: [x, y]) - ownerX String - ownerY String - @@unique([ownerX, ownerY]) - @@allow('all', auth() == owner) - } - `, - (model) => model.attributes[1].args[1].value, - `(user==null) ? - { OR: [] } : - { owner: { is: { x: user.x, y: user.y } } }`, - '{ x: "1", y: "2" }' - ); - - await check( - ` - model User { - x String - y String - t Test? - @@id([x, y]) - } - - model Test { - id String @id - owner User @relation(fields: [ownerX, ownerY], references: [x, y]) - ownerX String - ownerY String - @@unique([ownerX, ownerY]) - @@allow('all', auth() != owner) - } - `, - (model) => model.attributes[1].args[1].value, - `(user==null) ? - { AND: [] } : - { owner: { isNot: { x: user.x, y: user.y } } }`, - '{ x: "1", y: "2" }' - ); - - await check( - ` - model User { - x String - y String - t Test? - @@id([x, y]) - } - - model Test { - id String @id - owner User @relation(fields: [ownerX, ownerY], references: [x, y]) - ownerX String - ownerY String - @@unique([ownerX, ownerY]) - @@allow('all', auth().x == owner.x && auth().y == owner.y) - } - `, - (model) => model.attributes[1].args[1].value, - `{ - AND: [ - ((user?.x??null)==null) ? { OR: [] } : { owner: { x: { equals: (user?.x ?? null) } } }, - ((user?.y??null)==null) ? { OR: [] } : { owner: { y: { equals: (user?.y ?? null) } } } - ] - }`, - '{ x: "1", y: "2" }' - ); - }); - - it('auth check against nullable field', async () => { - await check( - ` - model User { - id String @id - t Test? - } - - model Test { - id String @id - owner User? @relation(fields: [ownerId], references: [id]) - ownerId String? @unique - @@allow('all', auth() == owner) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - owner: { - is: (user == null) ? null : { id: user.id } - } - }` - ); - - await check( - ` - model User { - id String @id - t Test? - } - - model Test { - id String @id - owner User? @relation(fields: [ownerId], references: [id]) - ownerId String? @unique - @@deny('all', auth() != owner) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - owner: { - isNot: (user == null) ? null : { id: user.id } - } - }` - ); - - await check( - ` - model User { - id String @id - t Test? - } - - model Test { - id String @id - owner User? @relation(fields: [ownerId], references: [id]) - ownerId String? @unique - @@allow('all', auth().id == owner.id) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.id??null)==null) ? { OR: [] } : { owner: { id: { equals: (user?.id ?? null) } } }` - ); - }); - - it('auth check short-circuit [TBD]', async () => { - await check( - ` - model User { - id String @id - t Test? - } - - model Test { - id String @id - owner User @relation(fields: [ownerId], references: [id]) - ownerId String @unique - value Int - @@allow('all', auth() != null && auth().id == owner.id && value > 0) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - AND: [ - { - AND: [ - (user!=null)?{AND:[]}:{OR:[]}, - ((user?.id??null)==null) ? {OR:[]} : { owner: { id: { equals: (user?.id??null) } } } - ] - }, - { value: { gt: 0 } } - ] - }` - ); - - await check( - ` - model User { - id String @id - t Test? - } - - model Test { - id String @id - owner User @relation(fields: [ownerId], references: [id]) - ownerId String @unique - value Int - @@deny('all', auth() == null || auth().id != owner.id || value <= 0) - } - `, - (model) => model.attributes[0].args[1].value, - `{ - OR: [ - { - OR: [ - (user==null)?{AND:[]}:{OR:[]}, - ((user?.id??null)==null) ? {AND:[]} : { owner : { id: { not: { equals: (user?.id??null) } } } } - ] - }, - { value: { lte: 0 } } - ] - }` - ); - }); - - it('auth using different model check', async () => { - await check( - ` - model Membership { - id String @id - t Test? - @@auth() - } - - model Test { - id String @id - owner Membership @relation(fields: [ownerId], references: [id]) - ownerId String @unique @allow('all', auth().id == owner.id) - value Int - @@allow('all', auth().id == owner.id) - - } - `, - (model) => { - const args = model.attributes[0].args[1]; - return args.value; - }, - `((user?.id??null)==null)?{ - OR:[] - }:{ - owner:{ - id:{ - equals:(user?.id??null) - } - } - }` - ); - }); - - it('relation field null check', async () => { - await check( - ` - model M { - id String @id - s String? - t Test @relation(fields: [tId], references: [id]) - tId String @unique - } - - model Test { - id String @id - m M? - @@allow('all', m == null || m.s == null) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - OR: [{ m: { is: null } }, { m: { s: { equals: null } } }] - }` - ); - - await check( - ` - model M { - id String @id - s String? - t Test @relation(fields: [tId], references: [id]) - tId String @unique - } - - model Test { - id String @id - m M? - @@deny('all', m != null || m.s != null) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - OR: [{ m: { isNot: null } }, { m: { s: { not: { equals: null } } } }] - } - ` - ); - }); - - it('filter operators field access', async () => { - await check( - ` - enum Role { - USER - ADMIN - } - model Test { - id String @id - role Role - @@allow('all', role in [USER, ADMIN]) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - role: { in: [Role.USER, Role.ADMIN] } - } - ` - ); - - await check( - ` - model Test { - id String @id - value String - @@allow('all', contains(value, 'foo')) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - value: { contains: 'foo' } - } - ` - ); - - await check( - ` - model Test { - id String @id - value String - @@allow('all', contains(value, 'foo', true)) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - value: { contains: 'foo', mode: 'insensitive' } - } - ` - ); - - await check( - ` - model Test { - id String @id - value String - @@allow('all', contains(value, 'foo', false)) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - value: { contains: 'foo' } - } - ` - ); - - await check( - ` - model Foo { - id String @id - value String - test Test @relation(fields: [testId], references: [id]) - testId String @unique - } - model Test { - id String @id - foo Foo? - @@allow('all', search(foo.value, 'foo')) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - foo: { - value: { search: 'foo' } - } - } - ` - ); - - await check( - ` - model Test { - id String @id - value String - @@allow('all', startsWith(value, 'foo') && endsWith(value, 'bar')) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - AND: [ { value: { startsWith: 'foo' } }, { value: { endsWith: 'bar' } } ] - } - ` - ); - - await check( - ` - model Test { - id String @id - value String - @@allow('all', !startsWith(value, 'foo')) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - NOT: { value: { startsWith: 'foo' } } - } - ` - ); - - await check( - ` - model Test { - id String @id - values Int[] - @@allow('all', has(values, 1)) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - values: { has: 1 } - } - ` - ); - - await check( - ` - model Test { - id String @id - values Int[] - @@allow('all', hasSome(values, [1, 2])) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - values: { hasSome: [1, 2] } - } - ` - ); - - await check( - ` - model Test { - id String @id - values Int[] - @@allow('all', hasEvery(values, [1, 2])) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - values: { hasEvery: [1, 2] } - } - ` - ); - - await check( - ` - model Test { - id String @id - values Int[] - @@allow('all', isEmpty(values)) - } - `, - (model) => model.attributes[0].args[1].value, - ` - { - values: { isEmpty: true } - } - ` - ); - }); - - it('filter operators non-field access', async () => { - const userInit = `{ id: 'user1', email: 'test@zenstack.dev', roles: [Role.ADMIN] }`; - const prelude = ` - enum Role { - USER - ADMIN - } - - model User { - id String @id - email String - roles Role[] - } - `; - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', ADMIN in auth().roles) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.roles?.includes(Role.ADMIN))??false)?{AND:[]}:{OR:[]}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - roles Role[] - @@allow('all', ADMIN in roles) - } - `, - (model) => model.attributes[0].args[1].value, - `{roles:{has:Role.ADMIN}}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', contains(auth().email, 'test')) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.email?.includes('test'))??false)?{AND:[]}:{OR:[]}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', contains(auth().email, 'test', true)) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.email?.toLowerCase().includes('test'?.toLowerCase()))??false)?{AND:[]}:{OR:[]}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', startsWith(auth().email, 'test')) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.email?.startsWith('test'))??false)?{AND:[]}:{OR:[]}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', endsWith(auth().email, 'test')) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.email?.endsWith('test'))??false)?{AND:[]}:{OR:[]}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', has(auth().roles, ADMIN)) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.roles?.includes(Role.ADMIN))??false)?{AND:[]}:{OR:[]}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', hasEvery(auth().roles, [ADMIN, USER])) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.roles)!==undefined?([Role.ADMIN,Role.USER]?.every((item)=>user?.roles?.includes(item))):false)?{AND:[]}:{OR:[]}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', hasSome(auth().roles, [USER, ADMIN])) - } - `, - (model) => model.attributes[0].args[1].value, - `((user?.roles)!==undefined?([Role.USER,Role.ADMIN]?.some((item)=>user?.roles?.includes(item))):false)?{AND:[]}:{OR:[]}`, - userInit - ); - - await check( - ` - ${prelude} - model Test { - id String @id - @@allow('all', isEmpty(auth().roles)) - } - `, - (model) => model.attributes[0].args[1].value, - `(!user?.roles||user?.roles?.length===0)?{AND:[]}:{OR:[]}`, - userInit - ); - }); - - it('now() function', async () => { - await check( - ` - model Test { - id String @id - createdAt DateTime @default(now()) - @@allow('all', createdAt <= now()) - } - `, - (model) => model.attributes[0].args[1].value, - `{ createdAt: { lte: (new Date()) } }` - ); - }); -}); - -async function check(schema: string, getExpr: (model: DataModel) => Expression, expected: string, userInit?: string) { - if (!schema.includes('datasource ')) { - schema = - ` - datasource db { - provider = 'postgresql' - url = 'dummy' - } - ` + schema; - } - - const model = await loadModel(schema); - const expr = getExpr(model.declarations.find((d) => isDataModel(d) && d.name === 'Test') as DataModel); - - const project = new Project({ - compilerOptions: { skipLibCheck: true }, - }); - - const { name: sourcePath } = tmp.fileSync({ postfix: '.ts' }); - const sf = project.createSourceFile(sourcePath, undefined, { - overwrite: true, - }); - - // inject enums - model.declarations - .filter((d) => isEnum(d)) - .map((e) => { - sf.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: e.name, - initializer: ` - { - ${(e as Enum).fields.map((f) => `${f.name}: "${f.name}"`).join(',\n')} - } - `, - }, - ], - }); - }); - - // inject user variable - sf.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [{ name: 'user', initializer: userInit ?? '{ id: "user1" }' }], - }); - - sf.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: 'expr', - initializer: (writer) => new ExpressionWriter(writer, { operationContext: 'read' }).write(expr), - }, - ], - }); - - await project.save(); - console.log('Source saved:', sourcePath); - - if (project.getPreEmitDiagnostics().length > 0) { - for (const d of project.getPreEmitDiagnostics()) { - console.warn(`${d.getLineNumber()}: ${d.getMessageText()}`); - } - throw new Error('Compilation errors occurred'); - } - - const outExpr = sf.getVariableDeclaration('expr'); - // console.log('Generated expr:\n', outExpr?.getText()); - - if (expected) { - const generatedExpr = outExpr?.getInitializer()?.getText(); - expect(generatedExpr && generatedExpr.replace(/\s+/g, '')).toBe(expected.replace(/\s+/g, '')); - } -} diff --git a/packages/schema/tests/generator/prisma-builder.test.ts b/packages/schema/tests/generator/prisma-builder.test.ts deleted file mode 100644 index 8144110de..000000000 --- a/packages/schema/tests/generator/prisma-builder.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { getDMMF } from '@zenstackhq/sdk/prisma'; -import { - AttributeArg, - AttributeArgValue, - FieldAttribute, - FieldReference, - FieldReferenceArg, - FunctionCall, - FunctionCallArg, - ModelFieldType, - PrismaModel, -} from '../../src/plugins/prisma/prisma-builder'; - -async function validate(model: PrismaModel) { - const content = model.toString(); - try { - return await getDMMF({ datamodel: content }); - } catch (err) { - console.error(`Failed to load DMMF: ${err}`); - throw err; - } -} - -describe('Prisma Builder Tests', () => { - it('datasource', async () => { - let model = new PrismaModel(); - model.addDataSource('db', [ - { name: 'provider', text: '"postgresql"' }, - { name: 'url', text: 'env("DATABASE_URL")' }, - ]); - await validate(model); - - model = new PrismaModel(); - model.addDataSource('db', [ - { name: 'provider', text: '"postgresql"' }, - { name: 'url', text: '"postgresql://postgres:abc123@localhost:5432/sample?schema=public"' }, - ]); - await validate(model); - }); - - it('enum', async () => { - const model = new PrismaModel(); - const _enum = model.addEnum('UserRole'); - _enum.addField('USER'); - _enum.addField('ADMIN'); - await validate(model); - }); - - it('generator', async () => { - const model = new PrismaModel(); - model.addGenerator('client', [{ name: 'provider', text: '"prisma-client-js"' }]); - await validate(model); - }); - - it('model', async () => { - const model = new PrismaModel(); - const dm = model.addModel('User'); - dm.addField('id', 'String', [new FieldAttribute('@id')]); - dm.addField('createdAt', 'DateTime', [ - new FieldAttribute('@default', [ - new AttributeArg(undefined, new AttributeArgValue('FunctionCall', new FunctionCall('now'))), - ]), - ]); - await validate(model); - }); - - it('relation', async () => { - const model = new PrismaModel(); - const user = model.addModel('User'); - user.addField('id', 'String', [new FieldAttribute('@id')]); - user.addField('posts', new ModelFieldType('Post', true)); - - const post = model.addModel('Post'); - post.addField('id', 'String', [new FieldAttribute('@id')]); - post.addField('user', 'User', [ - new FieldAttribute('@relation', [ - new AttributeArg( - 'fields', - new AttributeArgValue('Array', [new AttributeArgValue('FieldReference', 'userId')]) - ), - new AttributeArg( - 'references', - new AttributeArgValue('Array', [new AttributeArgValue('FieldReference', 'id')]) - ), - new AttributeArg('onDelete', new AttributeArgValue('FieldReference', new FieldReference('Cascade'))), - ]), - ]); - post.addField('userId', 'String'); - - await validate(model); - }); - - it('model attribute', async () => { - const model = new PrismaModel(); - const post = model.addModel('Post'); - post.addField('id', 'String', [new FieldAttribute('@id')]); - post.addField('slug', 'String'); - post.addField('space', 'String'); - post.addField('tsid', 'String', [ - new FieldAttribute('@default', [ - new AttributeArg( - undefined, - new AttributeArgValue( - 'FunctionCall', - new FunctionCall('dbgenerated', [new FunctionCallArg('"timestamp_id()"')]) - ) - ), - ]), - ]); - post.addAttribute('@@unique', [ - new AttributeArg( - 'fields', - new AttributeArgValue('Array', [ - new AttributeArgValue('FieldReference', new FieldReference('space')), - new AttributeArgValue( - 'FieldReference', - new FieldReference('slug', [new FieldReferenceArg('sort', 'Desc')]) - ), - ]) - ), - ]); - - await validate(model); - }); -}); diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts deleted file mode 100644 index be643351f..000000000 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ /dev/null @@ -1,569 +0,0 @@ -/// - -import { getDMMF } from '@zenstackhq/sdk/prisma'; -import fs from 'fs'; -import path from 'path'; -import tmp from 'tmp'; -import { loadDocument } from '../../src/cli/cli-util'; -import { PrismaSchemaGenerator } from '../../src/plugins/prisma/schema-generator'; -import { execSync } from '../../src/utils/exec-utils'; -import { loadModel } from '../utils'; - -tmp.setGracefulCleanup(); - -describe('Prisma generator test', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - const r = tmp.dirSync({ unsafeCleanup: true }); - console.log(`Project dir: ${r.name}`); - process.chdir(r.name); - - execSync('npm init -y', { stdio: 'ignore' }); - execSync('npm install prisma@6.19.x'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('datasource coverage', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env("DATABASE_URL") - directUrl = env("DATABASE_URL") - extensions = [pg_trgm, postgis(version: "3.3.2"), uuid_ossp(map: "uuid-ossp", schema: "extensions")] - schemas = ["auth", "public"] - } - - generator client { - provider = "prisma-client-js" - previewFeatures = ["multiSchema", "postgresqlExtensions"] - } - - plugin prisma { - provider = '@core/prisma' - } - - /// User roles - enum Role { - /// Admin role - ADMIN - /// Regular role - USER - - @@schema("auth") - } - - /// My user model - /// defined here - model User { - /// the id field - id String @id @allow('read', this == auth()) - role Role - - @@schema("auth") - @@allow('all', true) - @@deny('update', this != auth()) - } - - /** - * My post model - * defined here - */ - model Post { - id String @id - @@schema("public") - } - `); - - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: 'schema.prisma', - format: false, - customAttributesAsComments: true, - }); - - const content = fs.readFileSync('schema.prisma', 'utf-8'); - expect(content).toContain('provider = "postgresql"'); - expect(content).toContain('url = env("DATABASE_URL")'); - expect(content).toContain('directUrl = env("DATABASE_URL")'); - expect(content).toContain( - 'extensions = [pg_trgm, postgis(version: "3.3.2"), uuid_ossp(map: "uuid-ossp", schema: "extensions")]' - ); - expect(content).toContain('schemas = ["auth", "public"]'); - expect(content).toContain('/// My user model'); - expect(content).toContain(`/// @@allow('all', true)`); - expect(content).toContain(`/// the id field`); - expect(content).toContain(`/// @allow('read', this == auth())`); - expect(content).not.toContain('/// My post model'); - expect(content).toContain('/// User roles'); - expect(content).toContain('/// Admin role'); - expect(content).toContain('/// Regular role'); - await getDMMF({ datamodel: content }); - }); - - it('field type coverage', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - model User { - id String @id - age Int - serial BigInt - height Float - salary Decimal - activated Boolean - createdAt DateTime - metadata Json - content Bytes - unsupported Unsupported('foo') - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - format: false, - }); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - - expect(content).toContain('id String'); - expect(content).toContain('age Int'); - expect(content).toContain('serial BigInt'); - expect(content).toContain('height Float'); - expect(content).toContain('salary Decimal'); - expect(content).toContain('activated Boolean'); - expect(content).toContain('createdAt DateTime'); - expect(content).toContain('metadata Json'); - expect(content).toContain('content Bytes'); - expect(content).toContain('unsupported Unsupported("foo")'); - }); - - it('attribute function', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - model User { - id String @id @default(nanoid(6)) - x String @default(nanoid()) - y String @default(dbgenerated("gen_random_uuid()")) - z String @default(auth().id) - cuid String @default(cuid()) - cuid2 String @default(cuid(2)) - ulid String @default(ulid()) - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - generateClient: false, - }); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - - expect(content).toContain('@default(nanoid(6))'); - expect(content).toContain('@default(nanoid())'); - expect(content).toContain('@default(dbgenerated("gen_random_uuid()"))'); - expect(content).not.toContain('@default(auth().id)'); - expect(content).toContain('@default(cuid())'); - expect(content).toContain('@default(cuid(2))'); - expect(content).toContain('@default(ulid())'); - }); - - it('triple slash comments', async () => { - const model = await loadModel(` - datasource db { - provider = 'sqlite' - url = 'file:dev.db' - } - - /// This is a comment - model Foo { - id String @id - /// Comment for field value - value Int - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - customAttributesAsComments: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - expect(content).toContain('/// This is a comment'); - expect(content).toContain('/// Comment for field value'); - }); - - it('triple slash attribute pass-through', async () => { - const model = await loadModel(` - datasource db { - provider = 'sqlite' - url = 'file:dev.db' - } - - attribute @TypeGraphQL.omit(output: Any?, input: Any?) - attribute @TypeGraphQL.field(name: String) - - model User { - id Int @id - password String @TypeGraphQL.omit(output: true, input: true) - another String @TypeGraphQL.omit(input: ['update', 'where', 'orderBy']) - foo String @TypeGraphQL.field(name: 'bar') - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - customAttributesAsComments: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - expect(content).toContain(`/// @TypeGraphQL.omit(output: true, input: true)`); - expect(content).toContain(`/// @TypeGraphQL.omit(input: ['update', 'where', 'orderBy'])`); - expect(content).toContain(`/// @TypeGraphQL.field(name: 'bar')`); - }); - - it('model and field mapping', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - enum Role { - /// Admin role documentation line 1 - /// Admin role documentation line 2 - ADMIN @map('admin') - CUSTOMER @map('customer') - @@map('_Role') - } - - model User { - id Int @id - role Role @default(CUSTOMER) @map('_role') - - @@map('_User') - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - }); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - expect(content).toContain(`@@map("_User")`); - expect(content).toContain(`@map("_role")`); - expect(content).toContain(`@@map("_Role")`); - expect(content).toContain(`@map("admin")`); - expect(content).toContain(`@map("customer")`); - expect(content).toContain( - '/// Admin role documentation line 1\n' + ' /// Admin role documentation line 2\n' + ' ADMIN' - ); - }); - - it('attribute passthrough', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - model Foo { - id String @id - name String @prisma.passthrough('@unique()') - x Int - y Int - @@prisma.passthrough('@@index([x, y])') - } - - enum Role { - USER @prisma.passthrough('@map("__user")') - ADMIN @prisma.passthrough('@map("__admin")') - - @@prisma.passthrough('@@map("__role")') - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - }); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - expect(content).toContain('@unique()'); - expect(content).toContain('@@index([x, y])'); - }); - - it('multi schema', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - schemas = ['base', 'transactional'] - } - - generator client { - provider = "prisma-client-js" - previewFeatures = ["multiSchema"] - } - - model User { - id Int @id - orders Order[] - - @@schema("base") - } - - model Order { - id Int @id - user User @relation(fields: [id], references: [id]) - user_id Int - - @@schema("transactional") - } - - enum Size { - Small - Medium - Large - - @@schema("transactional") - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - generateClient: false, - format: false, - }); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - expect(content).toContain('@@schema("base")'); - expect(content).toContain('@@schema("base")'); - expect(content).toContain('schemas = ["base", "transactional"]'); - }); - - it('abstract model', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - abstract model Base { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - } - - abstract model Content { - title String - } - - model Post extends Base, Content { - published Boolean @default(false) - } - `); - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - generateClient: false, - }); - console.log('Generated:', name); - - const content = fs.readFileSync(name, 'utf-8'); - const dmmf = await getDMMF({ datamodel: content }); - - expect(dmmf.datamodel.models.length).toBe(1); - const post = dmmf.datamodel.models[0]; - expect(post.name).toBe('Post'); - expect(post.fields.length).toBe(5); - expect(post.fields.map((f) => f.name)).toEqual(expect.arrayContaining(['id', 'title', 'published'])); - }); - - it('abstract multi files', async () => { - const model = await loadDocument(path.join(__dirname, './zmodel/schema.zmodel')); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - generateClient: false, - customAttributesAsComments: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - const dmmf = await getDMMF({ datamodel: content }); - - expect(dmmf.datamodel.models.length).toBe(3); - expect(dmmf.datamodel.enums[0].name).toBe('UserRole'); - - const post = dmmf.datamodel.models.find((m) => m.name === 'Post'); - - expect(post?.documentation?.replace(/\s/g, '')).toBe( - `@@allow('read', owner == auth()) @@allow('delete', owner == auth())`.replace(/\s/g, '') - ); - - const todo = dmmf.datamodel.models.find((m) => m.name === 'Todo'); - expect(todo?.documentation?.replace(/\s/g, '')).toBe(`@@allow('read', owner == auth())`.replace(/\s/g, '')); - }); - - it('multiple level inheritance', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - abstract model Base { - id String @id @default(cuid()) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - } - - abstract model BaseDeletable extends Base { - deleted Boolean @default(false) @omit - - @@deny('read', deleted) - } - - model Test1 extends BaseDeletable { - @@allow('all', true) - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - format: true, - customAttributesAsComments: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - const expected = fs.readFileSync(path.join(__dirname, './prisma/multi-level-inheritance.prisma'), 'utf-8'); - - expect(content).toBe(expected); - }); - - it('format prisma', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - model Post { - id Int @id() @default(autoincrement()) - title String - content String? - published Boolean @default(false) - @@allow('read', published) - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - format: true, - customAttributesAsComments: true, - }); - - const content = fs.readFileSync(name, 'utf-8'); - const expected = fs.readFileSync(path.join(__dirname, './prisma/format.prisma'), 'utf-8'); - - expect(content).toBe(expected); - }); - - // TODO: revisit view after prisma 6.13 release - // eslint-disable-next-line jest/no-disabled-tests - it.skip('view support', async () => { - const model = await loadModel(` - datasource db { - provider = 'postgresql' - url = env('URL') - } - - generator client { - provider = "prisma-client-js" - previewFeatures = ["views"] - } - - view UserInfo { - id Int @unique - email String - name String - bio String - } - `); - - const { name } = tmp.fileSync({ postfix: '.prisma' }); - await new PrismaSchemaGenerator(model).generate({ - name: 'Prisma', - provider: '@core/prisma', - schemaPath: 'schema.zmodel', - output: name, - format: false, - generateClient: false, - }); - - const content = fs.readFileSync(name, 'utf-8'); - await getDMMF({ datamodel: content }); - }); -}); diff --git a/packages/schema/tests/generator/prisma/format.prisma b/packages/schema/tests/generator/prisma/format.prisma deleted file mode 100644 index 22c2d6187..000000000 --- a/packages/schema/tests/generator/prisma/format.prisma +++ /dev/null @@ -1,17 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -////////////////////////////////////////////////////////////////////////////////////////////// - -datasource db { - provider = "postgresql" - url = env("URL") -} - -/// @@allow('read', published) -model Post { - id Int @id() @default(autoincrement()) - title String - content String? - published Boolean @default(false) -} diff --git a/packages/schema/tests/generator/prisma/multi-level-inheritance.prisma b/packages/schema/tests/generator/prisma/multi-level-inheritance.prisma deleted file mode 100644 index 7a0707d7a..000000000 --- a/packages/schema/tests/generator/prisma/multi-level-inheritance.prisma +++ /dev/null @@ -1,19 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -////////////////////////////////////////////////////////////////////////////////////////////// - -datasource db { - provider = "postgresql" - url = env("URL") -} - -/// @@deny('read', deleted) -/// @@allow('all', true) -model Test1 { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - /// @omit - deleted Boolean @default(false) -} diff --git a/packages/schema/tests/generator/zmodel/schema.zmodel b/packages/schema/tests/generator/zmodel/schema.zmodel deleted file mode 100644 index 8d73977e2..000000000 --- a/packages/schema/tests/generator/zmodel/schema.zmodel +++ /dev/null @@ -1,18 +0,0 @@ -import "user/user" - -datasource db { - provider = 'postgresql' - url = env('URL') -} - -model Post extends Basic { - title String - content String? - - @@allow('delete', owner == auth()) -} - -model Todo extends Basic { - title String - isCompleted Boolean -} \ No newline at end of file diff --git a/packages/schema/tests/generator/zmodel/user/user.zmodel b/packages/schema/tests/generator/zmodel/user/user.zmodel deleted file mode 100644 index eb351a627..000000000 --- a/packages/schema/tests/generator/zmodel/user/user.zmodel +++ /dev/null @@ -1,28 +0,0 @@ -import "../schema" -model User { - id String @id() @default(uuid()) - email String @unique() - name String? - posts Post[] - todos Todo[] - role UserRole - - // make user profile public - @@allow('read', true) -} - - -enum UserRole { - USER - ADMIN -} - -abstract model Basic { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - - @@allow('read', owner == auth()) -} \ No newline at end of file diff --git a/packages/schema/tests/schema/abstract.test.ts b/packages/schema/tests/schema/abstract.test.ts deleted file mode 100644 index 6a4b69e49..000000000 --- a/packages/schema/tests/schema/abstract.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as fs from 'fs'; -import path from 'path'; -import { loadModel } from '../utils'; - -describe('Abstract Schema Tests', () => { - it('model loading', async () => { - const content = fs.readFileSync(path.join(__dirname, './abstract.zmodel'), { - encoding: 'utf-8', - }); - await loadModel(content); - }); - - it('empty inheritance', async () => { - await loadModel(` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - abstract model Base { - id Int @id @default(autoincrement()) - } - - model Foo extends Base {} - `); - }); - - it('multiple level inheritance', async () => { - await loadModel(` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - abstract model Base1 { - id String @id @default(cuid()) - } - - abstract model Base2 extends Base1 { - fieldA String - } - - model A extends Base2 { - field String - b B[] - } - - model B { - id String @id @default(cuid()) - a A @relation(fields: [aId], references: [id]) - aId String - } - - `); - }); - - it('multiple id fields from base', async () => { - await loadModel(` - abstract model Base { - id1 String - id2 String - value String - - @@id([id1, id2]) - } - - model Item1 extends Base { - x String - } - - model Item2 extends Base { - y String - } - `); - }); -}); diff --git a/packages/schema/tests/schema/abstract.zmodel b/packages/schema/tests/schema/abstract.zmodel deleted file mode 100644 index d49a95640..000000000 --- a/packages/schema/tests/schema/abstract.zmodel +++ /dev/null @@ -1,33 +0,0 @@ -datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') -} - -generator js { - provider = 'prisma-client-js' -} - -abstract model Base { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id]) - userId String -} - - -model Post extends Base { - title String - published Boolean @default(false) -} - -model Todo extends Base { - description String - isDone Boolean @default(false) -} - -model User { - id String @id - todos Todo[] - posts Post[] -} \ No newline at end of file diff --git a/packages/schema/tests/schema/all-features.zmodel b/packages/schema/tests/schema/all-features.zmodel deleted file mode 100644 index 6acd2a6ca..000000000 --- a/packages/schema/tests/schema/all-features.zmodel +++ /dev/null @@ -1,190 +0,0 @@ -datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - extensions = [pg_trgm, postgis(version: "3.3.2"), uuid_ossp(map: "uuid-ossp", schema: "extensions")] -} - -generator client { - provider = "prisma-client-js" - previewFeatures = ["multiSchema", "postgresqlExtensions"] -} - -plugin openapi { - provider = '@zenstackhq/openapi' - output = 'openapi.json' - securitySchemes = { - basic: { type: 'http', scheme: 'basic' }, - bearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, - apiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' } - } -} - -/* -* Sample model for a collaborative Todo app -*/ - -enum UserRole { - ADMIN - USER -} - -abstract model Base { - tag String -} - -/* - * Model for a space in which users can collaborate on Lists and Todos - */ -model Space extends Base { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String @length(4, 50) - slug String @length(4, 16) - owner User? @relation(fields: [ownerId], references: [id]) - ownerId String? - members SpaceUser[] - lists List[] - unsupported Unsupported('foo') - - // require login - @@deny('all', auth() == null) - - // everyone can create a space - @@allow('create', true) - - // any user in the space can read the space - @@allow('read', members?[user == auth()]) - - // space admin can update and delete - @@allow('update,delete', members?[user == auth() && role == ADMIN]) - - @@index([slug(ops: raw("gin_trgm_ops"))], type: Gin) -} - -/* - * Model representing membership of a user in a space - */ -model SpaceUser { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - role UserRole - @@unique([userId, spaceId]) - - // require login - @@deny('all', auth() == null) - - // space admin can create/update/delete - @@allow('create,update,delete', space.members?[user == auth() && role == ADMIN]) - - // user can read entries for spaces which he's a member of - @@allow('read', space.members?[user == auth()]) - - @@allow('read', role in [ADMIN, USER]) -} - -/* - * Model for a user - */ -model User { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email - password String? @password @omit - emailVerified DateTime? - name String? - ownedSpaces Space[] - spaces SpaceUser[] - image String? @url - lists List[] - todos Todo[] - - // can be created by anyone, even not logged in - @@allow('create', true) - - // can be read by users sharing any space - @@allow('read', spaces?[space.members?[user == auth()]]) - - // full access by oneself - @@allow('all', auth() == this) -} - -/* - * Model for a Todo list - */ -model List { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - title String @length(1, 100) - private Boolean @default(false) - todos Todo[] - - // require login - @@deny('all', auth() == null) - - // can be read by owner or space members (only if not private) - @@allow('read', owner == auth() || (space.members?[user == auth()] && !private)) - - // when create, owner must be set to current user, and user must be in the space - @@allow('create', owner == auth() && space.members?[user == auth()]) - - // when create, owner must be set to current user, and user must be in the space - // update is not allowed to change owner - @@allow('update', owner == auth()&& space.members?[user == auth()] && future().owner == owner) - - // can be deleted by owner - @@allow('delete', owner == auth()) -} - -/* - * Model for a single Todo - */ -model Todo { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - list List @relation(fields: [listId], references: [id], onDelete: Cascade) - listId String - title String @length(1, 100) - completedAt DateTime? - - // require login - @@deny('all', auth() == null) - - // owner has full access, also space members have full access (if the parent List is not private) - @@allow('all', list.owner == auth()) - @@allow('all', list.space.members?[user == auth()] && !list.private) - - // update is not allowed to change owner - @@deny('update', future().owner != owner) -} - -view SpaceWithMembers { - id String @unique - name String - slug String -} - -model Image { - id Int @id @default(autoincrement()) - metadata Json -} - -type Metadata { - width Int - height Int - format String -} diff --git a/packages/schema/tests/schema/cal-com.test.ts b/packages/schema/tests/schema/cal-com.test.ts deleted file mode 100644 index 05da241b9..000000000 --- a/packages/schema/tests/schema/cal-com.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as fs from 'fs'; -import path from 'path'; -import { loadModel } from '../utils'; - -describe('Cal.com Schema Tests', () => { - it('model loading', async () => { - const content = fs.readFileSync(path.join(__dirname, './cal-com.zmodel'), { - encoding: 'utf-8', - }); - await loadModel(content); - }); -}); diff --git a/packages/schema/tests/schema/cal-com.zmodel b/packages/schema/tests/schema/cal-com.zmodel deleted file mode 100644 index a32bd45a6..000000000 --- a/packages/schema/tests/schema/cal-com.zmodel +++ /dev/null @@ -1,659 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -generator client { - provider = "prisma-client-js" - previewFeatures = [] -} - -plugin enhancer { - provider = '@core/enhancer' - output = '.zenstack' -} - -enum SchedulingType { - ROUND_ROBIN @map("roundRobin") - COLLECTIVE @map("collective") -} - -enum PeriodType { - UNLIMITED @map("unlimited") - ROLLING @map("rolling") - RANGE @map("range") -} - -model Host { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - eventTypeId Int - isFixed Boolean @default(false) -} - -model EventType { - id Int @id @default(autoincrement()) - /// @zod.min(1) - title String - /// @zod.custom(imports.eventTypeSlug) - slug String - description String? - position Int @default(0) - /// @zod.custom(imports.eventTypeLocations) - locations Json? - length Int - hidden Boolean @default(false) - hosts Host[] - users User[] @relation("user_eventtype") - owner User? @relation("owner", fields: [userId], references: [id], onDelete: Cascade) - userId Int? - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - teamId Int? - hashedLink HashedLink? - bookings Booking[] - availability Availability[] - webhooks Webhook[] - destinationCalendar DestinationCalendar? - eventName String? - customInputs EventTypeCustomInput[] - /// @zod.custom(imports.eventTypeBookingFields) - bookingFields Json? - timeZone String? - periodType PeriodType @default(UNLIMITED) - periodStartDate DateTime? - periodEndDate DateTime? - periodDays Int? - periodCountCalendarDays Boolean? - requiresConfirmation Boolean @default(false) - /// @zod.custom(imports.recurringEventType) - recurringEvent Json? - disableGuests Boolean @default(false) - hideCalendarNotes Boolean @default(false) - minimumBookingNotice Int @default(120) - beforeEventBuffer Int @default(0) - afterEventBuffer Int @default(0) - seatsPerTimeSlot Int? - seatsShowAttendees Boolean? @default(false) - schedulingType SchedulingType? - schedule Schedule? @relation(fields: [scheduleId], references: [id]) - scheduleId Int? - // price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column. - price Int @default(0) - // currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column. - currency String @default("usd") - slotInterval Int? - /// @zod.custom(imports.EventTypeMetaDataSchema) - metadata Json? - /// @zod.custom(imports.successRedirectUrl) - successRedirectUrl String? - workflows WorkflowsOnEventTypes[] - /// @zod.custom(imports.bookingLimitsType) - bookingLimits Json? - @@unique([userId, slug]) - @@unique([teamId, slug]) -} - -model Credential { - id Int @id @default(autoincrement()) - // @@type is deprecated - type String - key Json - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int? - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - // How to make it a required column? - appId String? - destinationCalendars DestinationCalendar[] - invalid Boolean? @default(false) -} - -enum IdentityProvider { - CAL - GOOGLE - SAML -} - -model DestinationCalendar { - id Int @id @default(autoincrement()) - integration String - externalId String - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int? @unique - booking Booking[] - eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - eventTypeId Int? @unique - credentialId Int? - credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade) -} - -enum UserPermissionRole { - USER - ADMIN -} - -model User { - id Int @id @default(autoincrement()) - username String? @unique - name String? - /// @zod.email() - email String @unique - emailVerified DateTime? - password String? - bio String? - avatar String? - timeZone String @default("Europe/London") - weekStart String @default("Sunday") - // DEPRECATED - TO BE REMOVED - startTime Int @default(0) - endTime Int @default(1440) - // - bufferTime Int @default(0) - hideBranding Boolean @default(false) - theme String? - createdDate DateTime @default(now()) @map(name: "created") - trialEndsAt DateTime? - eventTypes EventType[] @relation("user_eventtype") - credentials Credential[] - teams Membership[] - bookings Booking[] - schedules Schedule[] - defaultScheduleId Int? - selectedCalendars SelectedCalendar[] - completedOnboarding Boolean @default(false) - locale String? - timeFormat Int? @default(12) - twoFactorSecret String? - twoFactorEnabled Boolean @default(false) - identityProvider IdentityProvider @default(CAL) - identityProviderId String? - availability Availability[] - invitedTo Int? - webhooks Webhook[] - brandColor String @default("#292929") - darkBrandColor String @default("#fafafa") - // the location where the events will end up - destinationCalendar DestinationCalendar? - away Boolean @default(false) - // participate in dynamic group booking or not - allowDynamicBooking Boolean? @default(true) - /// @zod.custom(imports.userMetadata) - metadata Json? - verified Boolean? @default(false) - role UserPermissionRole @default(USER) - disableImpersonation Boolean @default(false) - impersonatedUsers Impersonations[] @relation("impersonated_user") - impersonatedBy Impersonations[] @relation("impersonated_by_user") - apiKeys ApiKey[] - accounts Account[] - sessions Session[] - Feedback Feedback[] - ownedEventTypes EventType[] @relation("owner") - workflows Workflow[] - routingForms App_RoutingForms_Form[] @relation("routing-form") - verifiedNumbers VerifiedNumber[] - hosts Host[] - @@map(name: "users") -} - -model Team { - id Int @id @default(autoincrement()) - /// @zod.min(1) - name String - /// @zod.min(1) - slug String? @unique - logo String? - bio String? - hideBranding Boolean @default(false) - hideBookATeamMember Boolean @default(false) - members Membership[] - eventTypes EventType[] - workflows Workflow[] - createdAt DateTime @default(now()) - /// @zod.custom(imports.teamMetadataSchema) - metadata Json? - theme String? - brandColor String @default("#292929") - darkBrandColor String @default("#fafafa") - verifiedNumbers VerifiedNumber[] -} - -enum MembershipRole { - MEMBER - ADMIN - OWNER -} - -model Membership { - teamId Int - userId Int - accepted Boolean @default(false) - role MembershipRole - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - disableImpersonation Boolean @default(false) - @@id([userId, teamId]) -} - -model VerificationToken { - id Int @id @default(autoincrement()) - identifier String - token String @unique - expires DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - @@unique([identifier, token]) -} - -model BookingReference { - id Int @id @default(autoincrement()) - /// @zod.min(1) - type String - /// @zod.min(1) - uid String - meetingId String? - meetingPassword String? - meetingUrl String? - booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade) - bookingId Int? - externalCalendarId String? - deleted Boolean? - credentialId Int? -} - -model Attendee { - id Int @id @default(autoincrement()) - email String - name String - timeZone String - locale String? @default("en") - booking Booking? @relation(fields: [bookingId], references: [id]) - bookingId Int? -} - -enum BookingStatus { - CANCELLED @map("cancelled") - ACCEPTED @map("accepted") - REJECTED @map("rejected") - PENDING @map("pending") -} - -model Booking { - id Int @id @default(autoincrement()) - uid String @unique - user User? @relation(fields: [userId], references: [id]) - userId Int? - references BookingReference[] - eventType EventType? @relation(fields: [eventTypeId], references: [id]) - eventTypeId Int? - title String - description String? - customInputs Json? - /// @zod.custom(imports.bookingResponses) - responses Json? - startTime DateTime - endTime DateTime - attendees Attendee[] - location String? - createdAt DateTime @default(now()) - updatedAt DateTime? - status BookingStatus @default(ACCEPTED) - paid Boolean @default(false) - payment Payment[] - destinationCalendar DestinationCalendar? @relation(fields: [destinationCalendarId], references: [id]) - destinationCalendarId Int? - cancellationReason String? - rejectionReason String? - dynamicEventSlugRef String? - dynamicGroupSlugRef String? - rescheduled Boolean? - fromReschedule String? - recurringEventId String? - smsReminderNumber String? - workflowReminders WorkflowReminder[] - scheduledJobs String[] - /// @zod.custom(imports.bookingMetadataSchema) - metadata Json? -} - -model Schedule { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - eventType EventType[] - name String - timeZone String? - availability Availability[] - @@index([userId]) -} - -model Availability { - id Int @id @default(autoincrement()) - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int? - eventType EventType? @relation(fields: [eventTypeId], references: [id]) - eventTypeId Int? - days Int[] - startTime DateTime @db.Time - endTime DateTime @db.Time - date DateTime? @db.Date - Schedule Schedule? @relation(fields: [scheduleId], references: [id]) - scheduleId Int? - @@index([eventTypeId]) - @@index([scheduleId]) -} - -model SelectedCalendar { - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - integration String - externalId String - @@id([userId, integration, externalId]) -} - -enum EventTypeCustomInputType { - TEXT @map("text") - TEXTLONG @map("textLong") - NUMBER @map("number") - BOOL @map("bool") - RADIO @map("radio") - PHONE @map("phone") -} - -model EventTypeCustomInput { - id Int @id @default(autoincrement()) - eventTypeId Int - eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - label String - type EventTypeCustomInputType - /// @zod.custom(imports.customInputOptionSchema) - options Json? - required Boolean - placeholder String @default("") -} - -model ResetPasswordRequest { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String - expires DateTime -} - -enum ReminderType { - PENDING_BOOKING_CONFIRMATION -} - -model ReminderMail { - id Int @id @default(autoincrement()) - referenceId Int - reminderType ReminderType - elapsedMinutes Int - createdAt DateTime @default(now()) -} - -model Payment { - id Int @id @default(autoincrement()) - uid String @unique - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - appId String? - bookingId Int - booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade) - amount Int - fee Int - currency String - success Boolean - refunded Boolean - data Json - externalId String @unique -} - -enum WebhookTriggerEvents { - BOOKING_CREATED - BOOKING_RESCHEDULED - BOOKING_CANCELLED - FORM_SUBMITTED - MEETING_ENDED -} - -model Webhook { - id String @id @unique - userId Int? - eventTypeId Int? - /// @zod.url() - subscriberUrl String - payloadTemplate String? - createdAt DateTime @default(now()) - active Boolean @default(true) - eventTriggers WebhookTriggerEvents[] - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - appId String? - secret String? - @@unique([userId, subscriberUrl], name: "courseIdentifier") -} - -model Impersonations { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - impersonatedUser User @relation("impersonated_user", fields: [impersonatedUserId], references: [id], onDelete: Cascade) - impersonatedBy User @relation("impersonated_by_user", fields: [impersonatedById], references: [id], onDelete: Cascade) - impersonatedUserId Int - impersonatedById Int -} - -model ApiKey { - id String @id @unique @default(cuid()) - userId Int - note String? - createdAt DateTime @default(now()) - expiresAt DateTime? - lastUsedAt DateTime? - hashedKey String @unique() - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - appId String? -} - -model HashedLink { - id Int @id @default(autoincrement()) - link String @unique() - eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - eventTypeId Int @unique -} - -model Account { - id String @id @default(cuid()) - userId Int - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([provider, providerAccountId]) -} - -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId Int - expires DateTime - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) -} - -enum AppCategories { - calendar - messaging - other - payment - video - web3 - automation - analytics -} - -model App { - // The slug for the app store public page inside `/apps/[slug]` - slug String @id @unique - // The directory name for `/packages/app-store/[dirName]` - dirName String @unique - // Needed API Keys - keys Json? - // One or multiple categories to which this app belongs - categories AppCategories[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - credentials Credential[] - payments Payment[] - Webhook Webhook[] - ApiKey ApiKey[] - enabled Boolean @default(false) -} - -model App_RoutingForms_Form { - id String @id @default(cuid()) - description String? - routes Json? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String - fields Json? - user User @relation("routing-form", fields: [userId], references: [id], onDelete: Cascade) - userId Int - responses App_RoutingForms_FormResponse[] - disabled Boolean @default(false) - /// @zod.custom(imports.RoutingFormSettings) - settings Json? -} - -model App_RoutingForms_FormResponse { - id Int @id @default(autoincrement()) - formFillerId String @default(cuid()) - form App_RoutingForms_Form @relation(fields: [formId], references: [id], onDelete: Cascade) - formId String - response Json - createdAt DateTime @default(now()) - @@unique([formFillerId, formId]) -} - -model Feedback { - id Int @id @default(autoincrement()) - date DateTime @default(now()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - rating String - comment String? -} - -enum WorkflowTriggerEvents { - BEFORE_EVENT - EVENT_CANCELLED - NEW_EVENT - AFTER_EVENT - RESCHEDULE_EVENT -} - -enum WorkflowActions { - EMAIL_HOST - EMAIL_ATTENDEE - SMS_ATTENDEE - SMS_NUMBER - EMAIL_ADDRESS -} - -model WorkflowStep { - id Int @id @default(autoincrement()) - stepNumber Int - action WorkflowActions - workflowId Int - workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade) - sendTo String? - reminderBody String? - emailSubject String? - template WorkflowTemplates @default(REMINDER) - workflowReminders WorkflowReminder[] - numberRequired Boolean? - sender String? - numberVerificationPending Boolean @default(true) -} - -model Workflow { - id Int @id @default(autoincrement()) - name String - userId Int? - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - teamId Int? - activeOn WorkflowsOnEventTypes[] - trigger WorkflowTriggerEvents - time Int? - timeUnit TimeUnit? - steps WorkflowStep[] -} - -model WorkflowsOnEventTypes { - id Int @id @default(autoincrement()) - workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade) - workflowId Int - eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - eventTypeId Int -} - -model Deployment { - /// This is a single row table, so we use a fixed id - id Int @id @default(1) - logo String? - /// @zod.custom(imports.DeploymentTheme) - theme Json? - licenseKey String? - agreedLicenseAt DateTime? -} - -enum TimeUnit { - DAY @map("day") - HOUR @map("hour") - MINUTE @map("minute") -} - -model WorkflowReminder { - id Int @id @default(autoincrement()) - bookingUid String - booking Booking? @relation(fields: [bookingUid], references: [uid], onDelete: Cascade) - method WorkflowMethods - scheduledDate DateTime - referenceId String? @unique - scheduled Boolean - workflowStepId Int - workflowStep WorkflowStep @relation(fields: [workflowStepId], references: [id], onDelete: Cascade) - cancelled Boolean? -} - -enum WorkflowTemplates { - REMINDER - CUSTOM -} - -enum WorkflowMethods { - EMAIL - SMS -} - -model VerifiedNumber { - id Int @id @default(autoincrement()) - userId Int? - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - teamId Int? - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - phoneNumber String -} diff --git a/packages/schema/tests/schema/formatter.test.ts b/packages/schema/tests/schema/formatter.test.ts deleted file mode 100644 index 24917436d..000000000 --- a/packages/schema/tests/schema/formatter.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/// - -import { EmptyFileSystem } from 'langium'; -import { expectFormatting } from 'langium/test'; -import { createZModelServices } from '../../src/language-server/zmodel-module'; -const services = createZModelServices({ ...EmptyFileSystem }).ZModel; -const formatting = expectFormatting(services); - -describe('ZModelFormatter', () => { - it('declaration formatting', async () => { - await formatting({ - before: `datasource db { provider = 'postgresql' url = env('DATABASE_URL')} generator js {provider = 'prisma-client-js'} - plugin swrHooks {provider = '@zenstackhq/swr'output = 'lib/hooks'} - model User {id String @id name String? } - enum Role {ADMIN USER}`, - after: `datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') -} -generator js { - provider = 'prisma-client-js' -} -plugin swrHooks { - provider = '@zenstackhq/swr' - output = 'lib/hooks' -} -model User { - id String @id - name String? -} -enum Role { - ADMIN - USER -}`, - }); - }); -}); diff --git a/packages/schema/tests/schema/mutil-files/multi-files.test.ts b/packages/schema/tests/schema/mutil-files/multi-files.test.ts deleted file mode 100644 index b9c41ff19..000000000 --- a/packages/schema/tests/schema/mutil-files/multi-files.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import path from 'path'; -import { loadDocument } from '../../../src/cli/cli-util'; - -describe('Mutiple files Tests', () => { - it('model loading post', async () => { - await loadDocument(path.join(__dirname, './schema.zmodel')); - }); - - it('model loading user', async () => { - await loadDocument(path.join(__dirname, './user.zmodel')); - }); -}); diff --git a/packages/schema/tests/schema/mutil-files/schema.zmodel b/packages/schema/tests/schema/mutil-files/schema.zmodel deleted file mode 100644 index 1aa1a71db..000000000 --- a/packages/schema/tests/schema/mutil-files/schema.zmodel +++ /dev/null @@ -1,19 +0,0 @@ -import "user" - -datasource db { - provider="sqlite" - url="file:./dev.db" -} - -generator client { - provider = "prisma-client-js" -} - - -model Post { - id Int @id() @default(autoincrement()) - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - // author has full access - @@allow('all', auth() == author) -} \ No newline at end of file diff --git a/packages/schema/tests/schema/mutil-files/user.zmodel b/packages/schema/tests/schema/mutil-files/user.zmodel deleted file mode 100644 index 79fbbe969..000000000 --- a/packages/schema/tests/schema/mutil-files/user.zmodel +++ /dev/null @@ -1,10 +0,0 @@ -import "schema" -model User { - id Int @id() @default(autoincrement()) - email String @unique() - name String? - posts Post[] - - // make user profile public - @@allow('read', true) -} \ No newline at end of file diff --git a/packages/schema/tests/schema/parser.test.ts b/packages/schema/tests/schema/parser.test.ts deleted file mode 100644 index b34bf5844..000000000 --- a/packages/schema/tests/schema/parser.test.ts +++ /dev/null @@ -1,577 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -/// - -import { - ArrayExpr, - AttributeArg, - BinaryExpr, - BooleanLiteral, - ConfigArrayExpr, - ConfigInvocationArg, - ConfigInvocationExpr, - DataModel, - DataSource, - Enum, - FunctionDecl, - InvocationExpr, - MemberAccessExpr, - NumberLiteral, - ReferenceExpr, - StringLiteral, - UnaryExpr, -} from '@zenstackhq/language/ast'; -import { loadModel } from '../utils'; - -describe('Parsing Tests', () => { - it('data source', async () => { - const content = ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - directUrl = env('DATABASE_URL') - extensions = [pg_trgm, postgis(version: "3.3.2"), uuid_ossp(map: "uuid-ossp", schema: "extensions")] - schemas = ["auth", "public"] - } - `; - const doc = await loadModel(content, false); - expect(doc.declarations).toHaveLength(1); - const ds = doc.declarations[0] as DataSource; - - expect(ds.name).toBe('db'); - expect(ds.fields).toHaveLength(5); - - expect(ds.fields[0]).toEqual( - expect.objectContaining({ - name: 'provider', - value: expect.objectContaining({ value: 'postgresql' }), - }) - ); - expect(ds.fields[1].name).toBe('url'); - expect((ds.fields[1].value as InvocationExpr).function.ref?.name).toBe('env'); - expect((ds.fields[1].value as InvocationExpr).args[0].value.$type).toBe(StringLiteral); - - expect((ds.fields[3].value as ConfigArrayExpr).items[0].$type).toBe(ConfigInvocationExpr); - expect( - (((ds.fields[3].value as ConfigArrayExpr).items[1] as ConfigInvocationExpr).args[0] as ConfigInvocationArg) - .name - ).toBe('version'); - expect( - (((ds.fields[3].value as ConfigArrayExpr).items[1] as ConfigInvocationExpr).args[0] as ConfigInvocationArg) - .value.$type - ).toBe(StringLiteral); - - expect((ds.fields[4].value as ConfigArrayExpr).items[0].$type).toBe(StringLiteral); - }); - - it('enum simple', async () => { - const content = ` - enum UserRole { - USER - ADMIN - } - - model User { - id String @id - role UserRole @default(USER) - } - `; - const doc = await loadModel(content, false); - const enumDecl = doc.declarations[0]; - expect(enumDecl.name).toBe('UserRole'); - expect((enumDecl as Enum).fields.map((f) => f.name)).toEqual(expect.arrayContaining(['USER', 'ADMIN'])); - - const model = doc.declarations[1] as DataModel; - expect(model.fields[1].type.reference?.ref?.name).toBe('UserRole'); - - const attrVal = model.fields[1].attributes[0].args[0] as AttributeArg; - expect((attrVal.value as ReferenceExpr).target.ref?.name).toBe('USER'); - }); - - it('enum fields named with type name', async () => { - const content = ` - enum MyEnum { - DateTime - Int - String - } - - model User { - id String @id - role MyEnum @default(DateTime) - - @@allow('all', role == String) - } - `; - await loadModel(content); - }); - - it('enum dup name resolve', async () => { - const content = ` - datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - } - - enum FirstEnum { - E1 // used in both ENUMs - E2 - } - - enum SecondEnum { - E1 // used in both ENUMs - E3 - E4 - } - - model M { - id Int @id - first SecondEnum @default(E1) - second FirstEnum @default(E1) - } - `; - - const doc = await loadModel(content); - const firstEnum = doc.declarations.find((d) => d.name === 'FirstEnum'); - const secondEnum = doc.declarations.find((d) => d.name === 'SecondEnum'); - const m = doc.declarations.find((d) => d.name === 'M') as DataModel; - expect(m.fields[1].attributes[0].args[0].value.$resolvedType?.decl).toBe(secondEnum); - expect(m.fields[2].attributes[0].args[0].value.$resolvedType?.decl).toBe(firstEnum); - }); - - it('string escape', async () => { - const content = ` - model Example { - id Int @id - doubleQuote String @default("s\\"1") - singleQuote String @default('s\\'1') - json Json @default("{\\"theme\\": \\"light\\", \\"consoleDrawer\\": false}") - } - `; - const doc = await loadModel(content, false); - const model = doc.declarations[0] as DataModel; - expect((model.fields[1].attributes[0].args[0].value as StringLiteral).value).toBe('s"1'); - expect((model.fields[2].attributes[0].args[0].value as StringLiteral).value).toBe("s'1"); - expect((model.fields[3].attributes[0].args[0].value as StringLiteral).value).toBe( - '{"theme": "light", "consoleDrawer": false}' - ); - }); - - it('model field types', async () => { - const content = ` - model User { - id String @id - age Int - serial BigInt - height Float - salary Decimal - activated Boolean - createdAt DateTime - metadata Json - content Bytes - unsupported Unsupported('foo') - } - `; - const doc = await loadModel(content, false); - const model = doc.declarations[0] as DataModel; - expect(model.fields.map((f) => f.type.type)).toEqual( - expect.arrayContaining([ - 'String', - 'Int', - 'BigInt', - 'Float', - 'Decimal', - 'Boolean', - 'Json', - 'DateTime', - 'Bytes', - ]) - ); - expect(model.fields.find((f) => f.name === 'unsupported')?.type.unsupported?.value.value).toBe('foo'); - }); - - it('model field modifiers', async () => { - const content = ` - model User { - id String @id - name String? - tags String[] - tagsWithDefault String[] @default([]) - } - `; - const doc = await loadModel(content, false); - const model = doc.declarations[0] as DataModel; - expect(model.fields[1].type.optional).toBeTruthy(); - expect(model.fields[2].type.array).toBeTruthy(); - }); - - it('model field attributes', async () => { - const content = ` - model User { - id String @id - activated Boolean @default(false) @unique - } - `; - const doc = await loadModel(content, false); - const model = doc.declarations[0] as DataModel; - expect(model.fields[0].attributes[0].decl.ref?.name).toBe('@id'); - expect(model.fields[1].attributes[0].args[0].value.$type).toBe(BooleanLiteral); - expect(model.fields[1].attributes[1].decl.ref?.name).toBe('@unique'); - }); - - it('model attributes', async () => { - const content = ` - model Model { - id String @id - a String - b String - @@unique([a, b]) - @@unique([a(sort: Asc), b]) - @@unique(b(sort: Desc)) - } - `; - const doc = await loadModel(content, false); - const model = doc.declarations[0] as DataModel; - expect(model.attributes).toHaveLength(3); - expect(model.attributes[0].decl.ref?.name).toBe('@@unique'); - expect( - (model.attributes[0].args[0].value as ArrayExpr).items.map( - (item) => (item as ReferenceExpr).target.ref?.name - ) - ).toEqual(expect.arrayContaining(['a', 'b'])); - - expect(((model.attributes[1].args[0].value as ArrayExpr).items[0] as ReferenceExpr).args[0]).toEqual( - expect.objectContaining({ - name: 'sort', - }) - ); - - expect((model.attributes[2].args[0].value as ReferenceExpr).target.ref?.name).toBe('b'); - expect((model.attributes[2].args[0].value as ReferenceExpr).args[0]).toEqual( - expect.objectContaining({ - name: 'sort', - }) - ); - }); - - it('model relation', async () => { - const content = ` - model User { - id String @id - posts Post[] - } - - model Post { - id String @id - owner User @relation(references: [id], onDelete: Cascade, onUpdate: Cascade) - } - `; - const doc = await loadModel(content, false); - const models = doc.declarations as DataModel[]; - expect(models[0].fields[1].type.reference?.ref?.name).toBe('Post'); - expect(models[1].fields[1].type.reference?.ref?.name).toBe('User'); - }); - - it('policy expressions', async () => { - const content = ` - model Model { - id String @id - a Int - b Int - c Boolean - - @@deny('all', !c) - @@deny('all', a < 0) - // @@deny(a + b < 10) - } - `; - const doc = await loadModel(content, false); - const model = doc.declarations[0] as DataModel; - const attrs = model.attributes; - - expect(attrs[0].args[1].value.$type).toBe(UnaryExpr); - expect((attrs[0].args[1].value as UnaryExpr).operand.$type).toBe(ReferenceExpr); - - expect(attrs[1].args[1].value.$type).toBe(BinaryExpr); - expect((attrs[1].args[1].value as BinaryExpr).left.$type).toBe(ReferenceExpr); - expect((attrs[1].args[1].value as BinaryExpr).right.$type).toBe(NumberLiteral); - - // expect(attrs[2].args[0].value.$type).toBe(BinaryExpr); - // expect((attrs[2].args[0].value as BinaryExpr).left.$type).toBe( - // BinaryExpr - // ); - }); - - it('expression precedence and associativity', async () => { - const content = ` - model Model { - id String @id - a Int - b Int - c Boolean - foo Foo? - // @@deny(a + b * 2 > 0) - // @@deny((a + b) * 2 > 0) - @@deny('all', a > 0 && b < 0) - @@deny('all', a >= 0 && b <= 0) - @@deny('all', a == 0 || b != 0) - @@deny('all', !c || a > 0) - @@deny('all', !(c || a > 0)) - @@deny('all', !foo.x) - } - - model Foo { - id String @id - x Boolean - modelId String - model Model @relation(references: [id], fields: [modelId]) - } - `; - - await loadModel(content, false); - - const doc = await loadModel(content, false); - const attrs = (doc.declarations[0] as DataModel).attributes; - - // a > 0 && b < 0 - let arg = attrs[0].args[1].value; - expect(arg.$type).toBe(BinaryExpr); - expect((arg as BinaryExpr).operator).toBe('&&'); - expect((arg as BinaryExpr).left.$type).toBe(BinaryExpr); - expect(((arg as BinaryExpr).left as BinaryExpr).operator).toBe('>'); - - // a >= 0 && b <= 0 - arg = attrs[1].args[1].value; - expect(arg.$type).toBe(BinaryExpr); - expect((arg as BinaryExpr).operator).toBe('&&'); - expect((arg as BinaryExpr).left.$type).toBe(BinaryExpr); - expect(((arg as BinaryExpr).left as BinaryExpr).operator).toBe('>='); - - // a == 0 || b != 0 - arg = attrs[2].args[1].value; - expect(arg.$type).toBe(BinaryExpr); - expect((arg as BinaryExpr).operator).toBe('||'); - expect((arg as BinaryExpr).left.$type).toBe(BinaryExpr); - expect(((arg as BinaryExpr).left as BinaryExpr).operator).toBe('=='); - - // !c || a > 0 - arg = attrs[3].args[1].value; - expect(arg.$type).toBe(BinaryExpr); - expect((arg as BinaryExpr).operator).toBe('||'); - expect((arg as BinaryExpr).left.$type).toBe(UnaryExpr); - expect(((arg as BinaryExpr).left as UnaryExpr).operator).toBe('!'); - - // !(c || a > 0) - arg = attrs[4].args[1].value; - expect(arg.$type).toBe(UnaryExpr); - expect((arg as UnaryExpr).operator).toBe('!'); - - // !foo.x - arg = attrs[5].args[1].value; - expect(arg.$type).toBe(UnaryExpr); - expect((arg as UnaryExpr).operator).toBe('!'); - expect((arg as UnaryExpr).operand.$type).toBe(MemberAccessExpr); - - // // 1: a + b * 2 > 0 - - // // > - // expect((attrs[0].args[0].value as BinaryExpr).operator).toBe('>'); - - // // a + b * 2 - // expect((attrs[0].args[0].value as BinaryExpr).left.$type).toBe( - // BinaryExpr - // ); - - // // 0 - // expect((attrs[0].args[0].value as BinaryExpr).right.$type).toBe( - // LiteralExpr - // ); - - // // + - // expect( - // ((attrs[0].args[0].value as BinaryExpr).left as BinaryExpr).operator - // ).toBe('+'); - - // // a - // expect( - // ((attrs[0].args[0].value as BinaryExpr).left as BinaryExpr).left - // .$type - // ).toBe(ReferenceExpr); - - // // b * 2 - // expect( - // ((attrs[0].args[0].value as BinaryExpr).left as BinaryExpr).right - // .$type - // ).toBe(BinaryExpr); - - // // 2: (a + b) * 2 > 0 - - // // > - // expect((attrs[1].args[0].value as BinaryExpr).operator).toBe('>'); - - // // (a + b) * 2 - // expect((attrs[1].args[0].value as BinaryExpr).left.$type).toBe( - // BinaryExpr - // ); - - // // 0 - // expect((attrs[1].args[0].value as BinaryExpr).right.$type).toBe( - // LiteralExpr - // ); - - // // * - // expect( - // ((attrs[1].args[0].value as BinaryExpr).left as BinaryExpr).operator - // ).toBe('*'); - - // // (a + b) - // expect( - // ((attrs[1].args[0].value as BinaryExpr).left as BinaryExpr).left - // .$type - // ).toBe(BinaryExpr); - - // // a - // expect( - // ( - // ((attrs[1].args[0].value as BinaryExpr).left as BinaryExpr) - // .left as BinaryExpr - // ).left.$type - // ).toBe(ReferenceExpr); - - // // b - // expect( - // ( - // ((attrs[1].args[0].value as BinaryExpr).left as BinaryExpr) - // .left as BinaryExpr - // ).right.$type - // ).toBe(ReferenceExpr); - - // // 2 - // expect( - // ((attrs[1].args[0].value as BinaryExpr).left as BinaryExpr).right - // .$type - // ).toBe(LiteralExpr); - }); - - it('function', async () => { - const content = ` - model M { - id String @id - a Int - b Int - c N[] - @@deny('all', foo(a, b)) - @@deny('all', bar(c)) - } - - model N { - id String @id - x Int - } - - function foo(a: Int, b: Int): Boolean { - } - - function bar(items: N[]): Boolean { - } - `; - const doc = await loadModel(content, false); - const model = doc.declarations[0] as DataModel; - const foo = doc.declarations[2] as FunctionDecl; - const bar = doc.declarations[3] as FunctionDecl; - - expect(foo.name).toBe('foo'); - expect(foo.params.map((p) => p.type.type)).toEqual(expect.arrayContaining(['Int', 'Int'])); - - expect(bar.name).toBe('bar'); - expect(bar.params[0].type.reference?.ref?.name).toBe('N'); - expect(bar.params[0].type.array).toBeTruthy(); - - expect(model.attributes[0].args[1].value.$type).toBe(InvocationExpr); - }); - - it('member access', async () => { - const content = ` - model M { - id String @id - a N - @@deny('all', a.x.y < 0) - @@deny('all', foo(a)) - } - - model N { - id String @id - x P - } - - model P { - id String @id - y Int - } - - function foo(n: N): Boolean { - n.x < 0 - } - `; - await loadModel(content, false); - }); - - it('collection predicate', async () => { - const content = ` - model M { - id String @id - n N[] - @@deny('all', n?[x < 0]) - } - - model N { - id String @id - x Int - } - `; - await loadModel(content, false); - }); - - it('collection predicate chained', async () => { - const content = ` - model M { - n N[] - @@deny('all', n?[p?[x < 0]]) - } - - model N { - p P[] - } - - model P { - x Int - } - `; - await loadModel(content, false); - }); - - it('boolean prefix id', async () => { - const content = ` - model trueModel { - id String @id - isPublic Boolean @default(false) - trueText String? - falseText String? - @@allow('all', isPublic == true) - } - `; - await loadModel(content, false); - }); - - it('view support', async () => { - const content = ` - view UserInfo { - id Int @unique - email String - name String - bio String - } - `; - const doc = await loadModel(content, false); - expect((doc.declarations[0] as DataModel).isView).toBeTruthy(); - }); -}); diff --git a/packages/schema/tests/schema/sample-todo.test.ts b/packages/schema/tests/schema/sample-todo.test.ts deleted file mode 100644 index 40387604c..000000000 --- a/packages/schema/tests/schema/sample-todo.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as fs from 'fs'; -import path from 'path'; -import { loadModel } from '../utils'; - -describe('Sample Todo Schema Tests', () => { - it('model loading', async () => { - const content = fs.readFileSync(path.join(__dirname, './todo.zmodel'), { - encoding: 'utf-8', - }); - await loadModel(content); - }); -}); diff --git a/packages/schema/tests/schema/stdlib.test.ts b/packages/schema/tests/schema/stdlib.test.ts deleted file mode 100644 index ad637be7a..000000000 --- a/packages/schema/tests/schema/stdlib.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SchemaLoadingError } from '../utils'; -import { NodeFileSystem } from 'langium/node'; -import path from 'path'; -import { URI } from 'vscode-uri'; -import { createZModelServices } from '../../src/language-server/zmodel-module'; - -describe('Stdlib Tests', () => { - it('stdlib', async () => { - const { shared } = createZModelServices(NodeFileSystem); - const stdLib = shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file(path.resolve('src/res/stdlib.zmodel')) - ); - await shared.workspace.DocumentBuilder.build([stdLib], { - validationChecks: 'all', - }); - - const validationErrors = (stdLib.diagnostics ?? []).filter((e) => e.severity === 1); - if (validationErrors.length > 0) { - for (const validationError of validationErrors) { - const range = stdLib.textDocument.getText(validationError.range); - console.error( - `line ${validationError.range.start.line + 1}: ${validationError.message}${ - range ? ' [' + range + ']' : '' - }` - ); - } - throw new SchemaLoadingError(validationErrors); - } - }); -}); diff --git a/packages/schema/tests/schema/todo.zmodel b/packages/schema/tests/schema/todo.zmodel deleted file mode 100644 index 61e449fd7..000000000 --- a/packages/schema/tests/schema/todo.zmodel +++ /dev/null @@ -1,182 +0,0 @@ -/* -* Sample model for a collaborative Todo app -*/ - -/* - * Data source definition - */ -datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') -} - -generator js { - provider = 'prisma-client-js' -} - -plugin hooks { - provider = '@zenstackhq/swr' - output = 'lib/hooks' -} - -/* - * Enum for user's role in a space - */ -enum SpaceUserRole { - USER - ADMIN -} - -/* - * Model for a space in which users can collaborate on Lists and Todos - */ -model Space { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String @length(4, 50) - slug String @unique @regex('^[0-9a-zA-Z]{4,16}$') - members SpaceUser[] - lists List[] - - // require login - @@deny('all', auth() == null) - - // everyone can create a space - @@allow('create', true) - - // any user in the space can read the space - @@allow('read', members?[user == auth()]) - - // space admin can update and delete - @@allow('update,delete', members?[user == auth() && role == ADMIN]) -} - -/* - * Model representing membership of a user in a space - */ -model SpaceUser { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - role SpaceUserRole - @@unique([userId, spaceId]) - - // require login - @@deny('all', auth() == null) - - // space admin can create/update/delete - @@allow('create,update,delete', space.members?[user == auth() && role == ADMIN]) - - // user can read entries for spaces which he's a member of - @@allow('read', space.members?[user == auth()]) -} - -/* - * Model for a user - */ -model User { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email - emailVerified DateTime? - password String? @password @omit - name String? - spaces SpaceUser[] - image String? @url - lists List[] - todos Todo[] - - // next-auth - accounts Account[] - - // can be created by anyone, even not logged in - @@allow('create', true) - - // can be read by users sharing any space - @@allow('read', spaces?[space.members?[user == auth()]]) - - // full access by oneself - @@allow('all', auth() == this) -} - -/* - * Model for a Todo list - */ -model List { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - title String @length(1, 100) - private Boolean @default(false) - todos Todo[] - - // require login - @@deny('all', auth() == null) - - // can be read by owner or space members (only if not private) - @@allow('read', owner == auth() || (space.members?[user == auth()] && !private)) - - // when create, owner must be set to current user, and user must be in the space - @@allow('create', owner == auth() && space.members?[user == auth()]) - - // when create, owner must be set to current user, and user must be in the space - // update is not allowed to change owner - @@allow('update', owner == auth() && space.members?[user == auth()] && future().owner == owner) - - // can be deleted by owner - @@allow('delete', owner == auth()) -} - -/* - * Model for a single Todo - */ -model Todo { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - list List @relation(fields: [listId], references: [id], onDelete: Cascade) - listId String - title String @length(1, 100) - completedAt DateTime? - - // require login - @@deny('all', auth() == null) - - // owner has full access, also space members have full access (if the parent List is not private) - @@allow('all', list.owner == auth()) - @@allow('all', list.space.members?[user == auth()] && !list.private) - - // update cannot change owner - @@deny('update', future().owner != owner) -} - -// next-auth -model Account { - id String @id @default(uuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? - refresh_token_expires_in Int? - access_token String? - expires_at Int? - token_type String? - scope String? - id_token String? - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([provider, providerAccountId]) -} diff --git a/packages/schema/tests/schema/trigger-dev.test.ts b/packages/schema/tests/schema/trigger-dev.test.ts deleted file mode 100644 index c712ad25d..000000000 --- a/packages/schema/tests/schema/trigger-dev.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as fs from 'fs'; -import path from 'path'; -import { loadModel } from '../utils'; - -describe('Trigger.dev Schema Tests', () => { - it('model loading', async () => { - const content = fs.readFileSync(path.join(__dirname, './trigger-dev.zmodel'), { - encoding: 'utf-8', - }); - await loadModel(content); - }); -}); diff --git a/packages/schema/tests/schema/trigger-dev.zmodel b/packages/schema/tests/schema/trigger-dev.zmodel deleted file mode 100644 index 67be17825..000000000 --- a/packages/schema/tests/schema/trigger-dev.zmodel +++ /dev/null @@ -1,1039 +0,0 @@ -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -generator client { - provider = "prisma-client-js" - binaryTargets = ["native", "debian-openssl-1.1.x"] -} - -model User { - id String @id @default(cuid()) - email String @unique - - authenticationMethod AuthenticationMethod - accessToken String? - authenticationProfile Json? - authenticationExtraParams Json? - authIdentifier String? @unique - - displayName String? - name String? - avatarUrl String? - - admin Boolean @default(false) - isOnCloudWaitlist Boolean @default(false) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - featureCloud Boolean @default(false) - isOnHostedRepoWaitlist Boolean @default(false) - - marketingEmails Boolean @default(true) - confirmedBasicDetails Boolean @default(false) - - orgMemberships OrgMember[] - sentInvites OrgMemberInvite[] - apiVotes ApiIntegrationVote[] - - invitationCode InvitationCode? @relation(fields: [invitationCodeId], references: [id]) - invitationCodeId String? -} - -model InvitationCode { - id String @id @default(cuid()) - code String @unique - - users User[] - - createdAt DateTime @default(now()) -} - -enum AuthenticationMethod { - GITHUB - MAGIC_LINK -} - -model Organization { - id String @id @default(cuid()) - slug String @unique - title String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - environments RuntimeEnvironment[] - connections IntegrationConnection[] - endpoints Endpoint[] - jobs Job[] - jobVersions JobVersion[] - events EventRecord[] - jobRuns JobRun[] - - projects Project[] - members OrgMember[] - invites OrgMemberInvite[] - externalAccounts ExternalAccount[] - integrations Integration[] - sources TriggerSource[] -} - -model ExternalAccount { - id String @id @default(cuid()) - identifier String - metadata Json? - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - connections IntegrationConnection[] - events EventRecord[] - runs JobRun[] - schedules ScheduleSource[] - triggerSources TriggerSource[] - missingConnections MissingConnection[] - - @@unique([environmentId, identifier]) -} - -// This is a "global" table that store all the integration methods for all the integrations across all orgs -model IntegrationAuthMethod { - id String @id @default(cuid()) - key String - - name String - description String - type String - - client Json? - config Json? - scopes Json? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - integrations Integration[] - - definition IntegrationDefinition @relation(fields: [definitionId], references: [id], onDelete: Cascade, onUpdate: Cascade) - definitionId String - - help Json? - - @@unique([definitionId, key]) -} - -model IntegrationDefinition { - id String @id - name String - instructions String? - description String? - packageName String @default("") - - authMethods IntegrationAuthMethod[] - Integration Integration[] -} - -model Integration { - id String @id @default(cuid()) - - slug String - - title String? - description String? - - setupStatus IntegrationSetupStatus @default(COMPLETE) - authSource IntegrationAuthSource @default(HOSTED) - - definition IntegrationDefinition @relation(fields: [definitionId], references: [id], onDelete: Cascade, onUpdate: Cascade) - definitionId String - - authMethod IntegrationAuthMethod? @relation(fields: [authMethodId], references: [id], onDelete: Cascade, onUpdate: Cascade) - authMethodId String? - - connectionType ConnectionType @default(DEVELOPER) - - scopes String[] - - customClientReference SecretReference? @relation(fields: [customClientReferenceId], references: [id], onDelete: Cascade, onUpdate: Cascade) - customClientReferenceId String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - attempts ConnectionAttempt[] - connections IntegrationConnection[] - jobIntegrations JobIntegration[] - sources TriggerSource[] - missingConnections MissingConnection[] - RunConnection RunConnection[] - - @@unique([organizationId, slug]) -} - -enum IntegrationAuthSource { - HOSTED - LOCAL -} - -enum IntegrationSetupStatus { - MISSING_FIELDS - COMPLETE -} - -model IntegrationConnection { - id String @id @default(cuid()) - - connectionType ConnectionType @default(DEVELOPER) - - expiresAt DateTime? - metadata Json - scopes String[] - - dataReference SecretReference @relation(fields: [dataReferenceId], references: [id], onDelete: Cascade, onUpdate: Cascade) - dataReferenceId String - - integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - integrationId String - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade) - externalAccountId String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - runConnections RunConnection[] -} - -enum ConnectionType { - EXTERNAL - DEVELOPER -} - -model ConnectionAttempt { - id String @id @default(cuid()) - - securityCode String? - - redirectTo String @default("/") - - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - integration Integration @relation(fields: [integrationId], references: [id]) - integrationId String -} - -model OrgMember { - id String @id @default(cuid()) - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) - userId String - - role OrgMemberRole @default(MEMBER) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - environments RuntimeEnvironment[] - - @@unique([organizationId, userId]) -} - -enum OrgMemberRole { - ADMIN - MEMBER -} - -model OrgMemberInvite { - id String @id @default(cuid()) - token String @unique @default(cuid()) - email String - role OrgMemberRole @default(MEMBER) - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - inviter User @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade) - inviterId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([organizationId, email]) -} - -model RuntimeEnvironment { - id String @id @default(cuid()) - slug String - apiKey String @unique - pkApiKey String? - - type RuntimeEnvironmentType @default(DEVELOPMENT) - - autoEnableInternalSources Boolean @default(true) - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - projectId String - - //when the org member is deleted, it will keep the environment but set it to null - orgMember OrgMember? @relation(fields: [orgMemberId], references: [id], onDelete: SetNull, onUpdate: Cascade) - orgMemberId String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - endpoints Endpoint[] - jobVersions JobVersion[] - events EventRecord[] - jobRuns JobRun[] - requestDeliveries HttpSourceRequestDelivery[] - jobAliases JobAlias[] - JobQueue JobQueue[] - sources TriggerSource[] - eventDispatchers EventDispatcher[] - scheduleSources ScheduleSource[] - ExternalAccount ExternalAccount[] - - @@unique([projectId, slug, orgMemberId]) -} - -enum RuntimeEnvironmentType { - PRODUCTION - STAGING - DEVELOPMENT - PREVIEW -} - -model Project { - id String @id @default(cuid()) - slug String @unique - name String - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - environments RuntimeEnvironment[] - endpoints Endpoint[] - jobs Job[] - jobVersion JobVersion[] - events EventRecord[] - runs JobRun[] - sources TriggerSource[] -} - -model Endpoint { - id String @id @default(cuid()) - slug String - url String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - projectId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - indexingHookIdentifier String? - - jobVersions JobVersion[] - jobRuns JobRun[] - httpRequestDeliveries HttpSourceRequestDelivery[] - dynamictriggers DynamicTrigger[] - sources TriggerSource[] - indexings EndpointIndex[] - - @@unique([environmentId, slug]) -} - -model EndpointIndex { - id String @id @default(cuid()) - - endpoint Endpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade, onUpdate: Cascade) - endpointId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - source EndpointIndexSource @default(MANUAL) - sourceData Json? - reason String? - - data Json - stats Json -} - -enum EndpointIndexSource { - MANUAL - API - INTERNAL - HOOK -} - -model Job { - id String @id @default(cuid()) - slug String - title String - internal Boolean @default(false) - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - projectId String - - versions JobVersion[] - runs JobRun[] - integrations JobIntegration[] - aliases JobAlias[] - dynamicTriggers DynamicTrigger[] - - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - @@unique([projectId, slug]) -} - -model JobVersion { - id String @id @default(cuid()) - version String - eventSpecification Json - - properties Json? - - job Job @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade) - jobId String - - endpoint Endpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade, onUpdate: Cascade) - endpointId String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - projectId String - - queue JobQueue @relation(fields: [queueId], references: [id]) - queueId String - - startPosition JobStartPosition @default(INITIAL) - preprocessRuns Boolean @default(false) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - runs JobRun[] - integrations JobIntegration[] - aliases JobAlias[] - examples EventExample[] - dynamicTriggers DynamicTrigger[] - triggerSources TriggerSource[] - - @@unique([jobId, version, environmentId]) -} - -model EventExample { - id String @id @default(cuid()) - - slug String - name String - icon String? - - payload Json? - - jobVersion JobVersion @relation(fields: [jobVersionId], references: [id], onDelete: Cascade, onUpdate: Cascade) - jobVersionId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([slug, jobVersionId]) -} - -model JobQueue { - id String @id @default(cuid()) - name String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - jobCount Int @default(0) - maxJobs Int @default(100) - - runs JobRun[] - jobVersion JobVersion[] - - @@unique([environmentId, name]) -} - -model JobAlias { - id String @id @default(cuid()) - name String @default("latest") - value String - - version JobVersion @relation(fields: [versionId], references: [id], onDelete: Cascade, onUpdate: Cascade) - versionId String - - job Job @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade) - jobId String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - @@unique([jobId, environmentId, name]) -} - -model JobIntegration { - id String @id @default(cuid()) - key String - - version JobVersion @relation(fields: [versionId], references: [id], onDelete: Cascade, onUpdate: Cascade) - versionId String - - job Job @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade) - jobId String - - integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - integrationId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([versionId, key]) -} - -model RunConnection { - id String @id @default(cuid()) - key String - - authSource IntegrationAuthSource @default(HOSTED) - - run JobRun @relation(fields: [runId], references: [id], onDelete: Cascade, onUpdate: Cascade) - runId String - - connection IntegrationConnection? @relation(fields: [connectionId], references: [id], onDelete: Cascade, onUpdate: Cascade) - connectionId String? - - integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - integrationId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - tasks Task[] - - @@unique([runId, key]) -} - -model DynamicTrigger { - id String @id @default(cuid()) - type DynamicTriggerType @default(EVENT) - slug String - - endpoint Endpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade, onUpdate: Cascade) - endpointId String - - jobs Job[] - sources TriggerSource[] - registrations DynamicTriggerRegistration[] - - sourceRegistrationJob JobVersion? @relation(fields: [sourceRegistrationJobId], references: [id], onDelete: Cascade, onUpdate: Cascade) - sourceRegistrationJobId String? - - @@unique([endpointId, slug, type]) -} - -enum DynamicTriggerType { - EVENT - SCHEDULE -} - -model EventDispatcher { - id String @id @default(cuid()) - event String - source String - payloadFilter Json? - contextFilter Json? - manual Boolean @default(false) - - dispatchableId String - dispatchable Json - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - enabled Boolean @default(true) - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - registrations DynamicTriggerRegistration[] - scheduleSources ScheduleSource[] - - @@unique([dispatchableId, environmentId]) -} - -enum JobStartPosition { - INITIAL - LATEST -} - -model EventRecord { - id String @id @default(cuid()) - eventId String - name String - timestamp DateTime @default(now()) - payload Json - context Json? - sourceContext Json? - - source String @default("trigger.dev") - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - projectId String - - externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade) - externalAccountId String? - - deliverAt DateTime @default(now()) - deliveredAt DateTime? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - isTest Boolean @default(false) - runs JobRun[] - - @@unique([eventId, environmentId]) -} - -model JobRun { - id String @id @default(cuid()) - number Int - - job Job @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade) - jobId String - - version JobVersion @relation(fields: [versionId], references: [id], onDelete: Cascade, onUpdate: Cascade) - versionId String - - event EventRecord @relation(fields: [eventId], references: [id], onDelete: Cascade, onUpdate: Cascade) - eventId String - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - endpoint Endpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade, onUpdate: Cascade) - endpointId String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - projectId String - - queue JobQueue @relation(fields: [queueId], references: [id]) - queueId String - - externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade) - externalAccountId String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - queuedAt DateTime? - startedAt DateTime? - completedAt DateTime? - - properties Json? - - status JobRunStatus @default(PENDING) - output Json? - - timedOutAt DateTime? - timedOutReason String? - - isTest Boolean @default(false) - preprocess Boolean @default(false) - - tasks Task[] - runConnections RunConnection[] - missingConnections MissingConnection[] - executions JobRunExecution[] -} - -enum JobRunStatus { - PENDING - QUEUED - WAITING_ON_CONNECTIONS - PREPROCESSING - STARTED - SUCCESS - FAILURE - TIMED_OUT - ABORTED -} - -model JobRunExecution { - id String @id @default(cuid()) - - run JobRun @relation(fields: [runId], references: [id], onDelete: Cascade, onUpdate: Cascade) - runId String - - retryCount Int @default(0) - retryLimit Int @default(0) - retryDelayInMs Int @default(0) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - startedAt DateTime? - completedAt DateTime? - - error String? - - reason JobRunExecutionReason @default(EXECUTE_JOB) - status JobRunExecutionStatus @default(PENDING) - - resumeTask Task? @relation(fields: [resumeTaskId], references: [id], onDelete: Cascade, onUpdate: Cascade) - resumeTaskId String? - - graphileJobId String? -} - -enum JobRunExecutionReason { - PREPROCESS - EXECUTE_JOB -} - -enum JobRunExecutionStatus { - PENDING - STARTED - SUCCESS - FAILURE -} - -model Task { - id String @id - idempotencyKey String - displayKey String? - name String - icon String? - - status TaskStatus @default(PENDING) - delayUntil DateTime? - noop Boolean @default(false) - - description String? - properties Json? - outputProperties Json? - params Json? - output Json? - error String? - redact Json? - style Json? - operation String? - - startedAt DateTime? - completedAt DateTime? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - run JobRun @relation(fields: [runId], references: [id], onDelete: Cascade, onUpdate: Cascade) - runId String - - parent Task? @relation("TaskParent", fields: [parentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - parentId String? - - runConnection RunConnection? @relation(fields: [runConnectionId], references: [id], onDelete: Cascade, onUpdate: Cascade) - runConnectionId String? - - children Task[] @relation("TaskParent") - executions JobRunExecution[] - attempts TaskAttempt[] - - @@unique([runId, idempotencyKey]) -} - -enum TaskStatus { - PENDING - WAITING - RUNNING - COMPLETED - ERRORED -} - -model TaskAttempt { - id String @id @default(cuid()) - - number Int - - task Task @relation(fields: [taskId], references: [id], onDelete: Cascade, onUpdate: Cascade) - taskId String - - status TaskAttemptStatus @default(PENDING) - - error String? - - runAt DateTime? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([taskId, number]) -} - -enum TaskAttemptStatus { - PENDING - STARTED - COMPLETED - ERRORED -} - -model SecretReference { - id String @id @default(cuid()) - key String @unique - provider SecretStoreProvider @default(DATABASE) - - connections IntegrationConnection[] - integrations Integration[] - triggerSources TriggerSource[] - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -enum SecretStoreProvider { - DATABASE - AWS_PARAM_STORE -} - -/// Used when the provider = "database". Not recommended outside of local development. -model SecretStore { - key String @unique - value Json - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model TriggerSource { - id String @id @default(cuid()) - - key String - params Json? - - channel TriggerChannel @default(HTTP) - channelData Json? - - events TriggerSourceEvent[] - - secretReference SecretReference @relation(fields: [secretReferenceId], references: [id], onDelete: Cascade, onUpdate: Cascade) - secretReferenceId String - - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - organizationId String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - endpoint Endpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade, onUpdate: Cascade) - endpointId String - - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - projectId String - - integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - integrationId String - - dynamicTrigger DynamicTrigger? @relation(fields: [dynamicTriggerId], references: [id], onDelete: Cascade, onUpdate: Cascade) - dynamicTriggerId String? - - externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade) - externalAccountId String? - - sourceRegistrationJob JobVersion? @relation(fields: [sourceRegistrationJobId], references: [id], onDelete: Cascade, onUpdate: Cascade) - sourceRegistrationJobId String? - - dynamicSourceId String? - dynamicSourceMetadata Json? - - active Boolean @default(false) - interactive Boolean @default(false) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - httpDeliveries HttpSourceRequestDelivery[] - registrations DynamicTriggerRegistration[] - - @@unique([key, environmentId]) -} - -enum TriggerChannel { - HTTP - SQS - SMTP -} - -model TriggerSourceEvent { - id String @id @default(cuid()) - name String - - source TriggerSource @relation(fields: [sourceId], references: [id], onDelete: Cascade, onUpdate: Cascade) - sourceId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - registered Boolean @default(false) - - @@unique([name, sourceId]) -} - -model DynamicTriggerRegistration { - id String @id @default(cuid()) - - key String - - dynamicTrigger DynamicTrigger @relation(fields: [dynamicTriggerId], references: [id], onDelete: Cascade, onUpdate: Cascade) - dynamicTriggerId String - - eventDispatcher EventDispatcher @relation(fields: [eventDispatcherId], references: [id], onDelete: Cascade, onUpdate: Cascade) - eventDispatcherId String - - source TriggerSource @relation(fields: [sourceId], references: [id], onDelete: Cascade, onUpdate: Cascade) - sourceId String - - metadata Json? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([key, dynamicTriggerId]) -} - -model HttpSourceRequestDelivery { - id String @id @default(cuid()) - url String - method String - headers Json - body Bytes? - - source TriggerSource @relation(fields: [sourceId], references: [id], onDelete: Cascade, onUpdate: Cascade) - sourceId String - - endpoint Endpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade, onUpdate: Cascade) - endpointId String - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deliveredAt DateTime? -} - -model ScheduleSource { - id String @id @default(cuid()) - - key String - schedule Json - - environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - - dispatcher EventDispatcher @relation(fields: [dispatcherId], references: [id], onDelete: Cascade, onUpdate: Cascade) - dispatcherId String - - lastEventTimestamp DateTime? - - workerJobId String? - - active Boolean @default(false) - - metadata Json? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade) - externalAccountId String? - - @@unique([key, environmentId]) -} - -model MissingConnection { - id String @id @default(cuid()) - - resolved Boolean @default(false) - - runs JobRun[] - - integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade, onUpdate: Cascade) - integrationId String - - connectionType ConnectionType @default(DEVELOPER) - - externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade) - externalAccountId String? - - accountIdentifier String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([integrationId, connectionType, externalAccountId]) - @@unique([integrationId, connectionType, accountIdentifier]) -} - -model ApiIntegrationVote { - id String @id @default(cuid()) - - apiIdentifier String - - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) - userId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([apiIdentifier, userId]) -} \ No newline at end of file diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts deleted file mode 100644 index 3e0553ee2..000000000 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ /dev/null @@ -1,1394 +0,0 @@ -/// - -import { loadModel, loadModelWithError } from '../../utils'; - -describe('Attribute tests', () => { - const prelude = ` - datasource db { - provider = "postgresql" - url = "url" - } - `; - - it('builtin field attributes', async () => { - await loadModel(` - ${prelude} - model M { - x String @id @default("abc") @unique @map("_id") - y DateTime @updatedAt - } - `); - }); - - it('field attribute type checking', async () => { - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id(123) - } - `) - ).toContain(`Unexpected unnamed argument`); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id() @default(value:'def', 'abc') - } - `) - ).toContain(`Unexpected unnamed argument`); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id() @default('abc', value:'def') - } - `) - ).toContain(`Parameter "value" is already provided`); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id() @default(123) - } - `) - ).toContain(`Value is not assignable to parameter`); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id() @default() - } - `) - ).toContain(`Required parameter not provided: value`); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id() @default('abc', value: 'def') - } - `) - ).toContain(`Parameter "value" is already provided`); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id() @default(foo: 'abc') - } - `) - ).toContain(`Attribute "@default" doesn't have a parameter named "foo"`); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id() - dt DateTime @default('2020abc') - } - `) - ).toContain('Value is not assignable to parameter'); - - // auto-convert of string to date time - await loadModel(` - ${prelude} - model M { - id String @id() - dt DateTime @default('2000-01-01T00:00:00Z') - } - `); - - // auto-convert of string to bytes - await loadModel(` - ${prelude} - model M { - id String @id() - dt Bytes @default('abc123') - } - `); - }); - - it('field attribute coverage', async () => { - await loadModel(` - ${prelude} - model A { - id String @id - } - - model B { - id String @id() - } - - model C { - id String @id(map: "__id") - } - - model D { - id String @id - x String @default("x") - } - - model E { - id String @id - x String @default(value: "x") - } - - model F { - id String @id - x String @default(uuid()) - } - - model G { - id String @id - x Int @default(autoincrement()) - } - - model H { - id String @id - x String @unique() - } - `); - }); - - it('model attribute coverage', async () => { - await loadModel(` - ${prelude} - model A { - x Int - y String - @@id([x, y], name: 'x_y', map: '_x_y', length: 10, sort: Asc, clustered: true) - } - - model B { - id String @id(map: '_id', length: 10, sort: Asc, clustered: true) - } - `); - - await loadModel(` - ${prelude} - model A { - id String @id - x Int - y String - @@unique([x, y], name: 'x_y', map: '_x_y', length: 10, sort: Asc, clustered: true) - } - `); - - await loadModel(` - ${prelude} - model A { - id String @id - x Int - y String - @@unique(fields: [x, y]) - } - `); - - await loadModel(` - ${prelude} - model A { - id String @id - x Int @unique(map: '_x', length: 10, sort: Asc, clustered: true) - } - `); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - y String - @@unique([x, z]) - } - `) - ).toContain(`Could not resolve reference to ReferenceTarget named 'z'.`); - - await loadModel(` - ${prelude} - model A { - id String @id - x Int - y String - @@index([x, y]) - } - - model B { - id String @id - x Int - y String - @@index([x(sort: Asc), y(sort: Desc)], name: 'myindex', map: '_myindex', length: 10, sort: Asc, clustered: true, type: BTree) - } - `); - - await loadModel(` - ${prelude} - model A { - id String @id - x String - y String - z String - @@fulltext([x, y, z]) - } - - model B { - id String @id - x String - y String - z String - @@fulltext([x, y, z], map: "n") - } - `); - - await loadModel(` - ${prelude} - model A { - id String @id - x Int @map("_x") - y String - @@map("__A") - } - `); - - await loadModel(` - ${prelude} - enum Role { - ADMIN @map("admin") - CUSTOMER @map("customer") - @@map("_Role") - } - `); - }); - - it('type modifier attribute coverage', async () => { - await loadModel(` - ${prelude} - - model _String { - id String @id - _string String @db.String - _string1 String @db.String(1) - _text String @db.Text - _ntext String @db.NText - _char String @db.Char(10) - _nchar String @db.NChar(10) - _varchar String @db.VarChar(10) - _nvarChar String @db.NVarChar(10) - _catalogSingleChar String @db.CatalogSingleChar - _tinyText String @db.TinyText - _mediumText String @db.MediumText - _longText String @db.LongText - _bit String @db.Bit - _bit1 String @db.Bit(1) - _varbit String @db.VarBit - _varbit1 String @db.VarBit(1) - _uuid String @db.Uuid - _uniqueIdentifier String @db.UniqueIdentifier - _xml String @db.Xml - _inet String @db.Inet - _citext String @db.Citext - } - - model _Boolean { - id String @id - _boolean Boolean @db.Boolean - _bit Boolean @db.Bit - _bit1 Boolean @db.Bit(1) - _tinyInt Boolean @db.TinyInt - _tinyInt1 Boolean @db.TinyInt(1) - } - - model _Int { - id String @id - _int Int @db.Int - _integer Int @db.Integer - _smallInt Int @db.SmallInt - _oid Int @db.Oid - _unsignedInt Int @db.UnsignedInt - _unsignedSmallInt Int @db.UnsignedSmallInt - _mediumInt Int @db.MediumInt - _unsignedMediumInt Int @db.UnsignedMediumInt - _unsignedTinyInt Int @db.UnsignedTinyInt - _year Int @db.Year - _int4 Int @db.Int4 - _int2 Int @db.Int2 - } - - model _BigInt { - id String @id - _bigInt BigInt @db.BigInt - _unsignedBigInt BigInt @db.UnsignedBigInt - _int8 BigInt @db.Int8 - } - - model _FloatDecimal { - id String @id - _float Float @db.Float - _decimal Decimal @db.Decimal - _decimal1 Decimal @db.Decimal(10, 2) - _doublePrecision Float @db.DoublePrecision - _real Float @db.Real - _double Float @db.Double - _money Float @db.Money - _money1 Decimal @db.Money - _smallMoney Float @db.SmallMoney - _float8 Float @db.Float8 - _float4 Float @db.Float4 - } - - model _DateTime { - id String @id - _dateTime DateTime @db.DateTime - _dateTime2 DateTime @db.DateTime2 - _smallDateTime DateTime @db.SmallDateTime - _dateTimeOffset DateTime @db.DateTimeOffset - _timestamp DateTime @db.Timestamp - _timestamp1 DateTime @db.Timestamp(1) - _timestamptz DateTime @db.Timestamptz - _timestamptz1 DateTime @db.Timestamptz(1) - _date DateTime @db.Date - _time DateTime @db.Time - _time1 DateTime @db.Time(1) - _timetz DateTime @db.Timetz - _timetz1 DateTime @db.Timetz(1) - } - - model _Json { - id String @id - _json Json @db.Json - _jsonb Json @db.JsonB - } - - model _Bytes { - id String @id - _bytes Bytes @db.Bytes - _byteA Bytes @db.ByteA - _longBlob Bytes @db.LongBlob - _binary Bytes @db.Binary - _varBinary Bytes @db.VarBinary - _varBinarySized Bytes @db.VarBinary(100) - _tinyBlob Bytes @db.TinyBlob - _blob Bytes @db.Blob - _mediumBlob Bytes @db.MediumBlob - _image Bytes @db.Image - } - `); - }); - - it('attribute function coverage', async () => { - await loadModel(` - ${prelude} - model User { id String @id } - - model A { - id String @id @default(uuid()) - id1 String @default(cuid()) - nanodId String @default(nanoid()) - nanodIdWithLength String @default(nanoid(3)) - created DateTime @default(now()) - serial Int @default(autoincrement()) - foo String @default(dbgenerated("gen_random_uuid()")) - @@allow('all', auth() != null) - } - `); - }); - - it('attribute function check', async () => { - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id @default(foo()) - } - `) - ).toContain(`Could not resolve reference to FunctionDecl named 'foo'.`); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id Int @id @default(uuid()) - } - `) - ).toContain(`Value is not assignable to parameter`); - }); - - it('policy expressions', async () => { - await loadModel(` - ${prelude} - model A { - id String @id - x Int - x1 Int - y DateTime - y1 DateTime - z Float - z1 Decimal - foo Boolean - bar Boolean - - @@allow('all', x > 0) - @@allow('all', x > x1) - @@allow('all', y >= y1) - @@allow('all', z < z1) - @@allow('all', z1 < z) - @@allow('all', x < z) - @@allow('all', x < z1) - @@allow('all', foo && bar) - @@allow('all', foo || bar) - @@allow('all', !foo) - } - `); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x String - @@allow('all', x > 0) - } - `) - ).toContain('invalid operand type for ">" operator'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x String - @@allow('all', x < 0) - } - `) - ).toContain('invalid operand type for "<" operator'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x String - y String - @@allow('all', x < y) - } - `) - ).toContain('invalid operand type for "<" operator'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x String - y String - @@allow('all', x <= y) - } - `) - ).toContain('invalid operand type for "<=" operator'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - y DateTime - @@allow('all', x <= y) - } - `) - ).toContain('incompatible operand types'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x String - y String - @@allow('all', x && y) - } - `) - ).toContain('invalid operand type for "&&" operator'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x String - y String - @@allow('all', x || y) - } - `) - ).toContain('invalid operand type for "||" operator'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - @@allow('all', x == this) - } - `) - ).toContain('incompatible operand types'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - @@allow('all', this != x) - } - `) - ).toContain('incompatible operand types'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - b B? - @@allow('all', b == this) - } - model B { - id String @id - a A? @relation(fields: [aId], references: [id]) - aId String - } - `) - ).toContain('incompatible operand types'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - b B? - @@allow('all', this != b) - } - model B { - id String @id - a A? @relation(fields: [aId], references: [id]) - aId String - } - `) - ).toContain('incompatible operand types'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - y Int[] - - @@allow(true, x == y) - } - `) - ).toContain('incompatible operand types'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - y Int[] - - @@allow(true, x > y) - } - `) - ).toContain('operand cannot be an array'); - - expect( - await loadModelWithError(` - ${prelude} - model User { - id Int @id - foo Foo @relation(fields: [fooId], references: [id]) - fooId Int - } - - model Foo { - id Int @id - users User[] - - @@allow('all', users == auth()) - } - `) - ).toContain('incompatible operand types'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - other A? @relation('other', fields: [otherId], references: [id]) - otherId String? @unique - holder A? @relation('other') - @@allow('all', other == this) - } - `) - ).toContain('comparison between model-typed fields are not supported'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - other A? @relation('other', fields: [otherId], references: [id]) - otherId String? @unique - holder A? @relation('other') - @@allow('all', this != other) - } - `) - ).toContain('comparison between model-typed fields are not supported'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - other A? @relation('other', fields: [otherId], references: [id]) - otherId String? @unique - holder A? @relation('other') - other1 A? @relation('other1', fields: [otherId1], references: [id]) - other1Id String? @unique - holder1 A? @relation('other1') - @@allow('all', other == other1) - } - `) - ).toContain('comparison between model-typed fields are not supported'); - - expect( - await loadModelWithError(` - ${prelude} - model User { - id Int @id - lists List[] - todos Todo[] - } - - model List { - id Int @id - user User @relation(fields: [userId], references: [id]) - userId Int - todos Todo[] - } - - model Todo { - id Int @id - user User @relation(fields: [userId], references: [id]) - userId Int - list List @relation(fields: [listId], references: [id]) - listId Int - - @@allow('read', list.user.id == userId) - } - - `) - ).toContain('comparison between fields of different models is not supported in model-level "read" rules'); - - expect( - await loadModelWithError(` - ${prelude} - model User { - id Int @id - lists List[] - todos Todo[] - value Int - } - - model List { - id Int @id - user User @relation(fields: [userId], references: [id]) - userId Int - todos Todo[] - } - - model Todo { - id Int @id - user User @relation(fields: [userId], references: [id]) - userId Int - list List @relation(fields: [listId], references: [id]) - listId Int - value Int - - @@allow('all', list.user.value > value) - } - - `) - ).toContain('comparison between fields of different models is not supported in model-level "read" rules'); - - expect( - await loadModel(` - ${prelude} - model User { - id Int @id - lists List[] - todos Todo[] - } - - model List { - id Int @id - user User @relation(fields: [userId], references: [id]) - userId Int - todos Todo[] - } - - model Todo { - id Int @id - user User @relation(fields: [userId], references: [id]) - userId Int - list List @relation(fields: [listId], references: [id]) - listId Int - - @@allow('create', list.user.id == userId) - } - - `) - ).toBeTruthy(); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - b B? - c C? - @@allow('all', b == c) - } - model B { - id String @id - a A? @relation(fields: [aId], references: [id]) - aId String - } - model C { - id String @id - a A? @relation(fields: [aId], references: [id]) - aId String - } - `) - ).toContain('incompatible operand types'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int - b B? - c C? - @@allow('all', b != c) - } - model B { - id String @id - a A? @relation(fields: [aId], references: [id]) - aId String - } - model C { - id String @id - a A? @relation(fields: [aId], references: [id]) - aId String - } - `) - ).toContain('incompatible operand types'); - }); - - it('policy filter function check', async () => { - await loadModel(` - ${prelude} - enum E { - E1 - E2 - } - - model User { - id String @id - e E - } - - model N { - id String @id - e E - es E[] - s String - i Int - m M @relation(fields: [mId], references: [id]) - mId String @unique - } - - model M { - id String @id - s String - e E - es E[] - n N? - - @@allow('all', e in [E1, E2]) - @@allow('all', contains(s, 'a')) - @@allow('all', contains(s, 'a', true)) - @@allow('all', search(s, 'a')) - @@allow('all', startsWith(s, 'a')) - @@allow('all', endsWith(s, 'a')) - @@allow('all', has(es, E1)) - @@allow('all', has(es, auth().e)) - @@allow('all', hasSome(es, [E1])) - @@allow('all', hasEvery(es, [E1])) - @@allow('all', isEmpty(es)) - - @@allow('all', n.e in [E1, E2]) - @@allow('all', n.i in [1, 2]) - @@allow('all', contains(n.s, 'a')) - @@allow('all', contains(n.s, 'a', true)) - @@allow('all', search(n.s, 'a')) - @@allow('all', startsWith(n.s, 'a')) - @@allow('all', endsWith(n.s, 'a')) - @@allow('all', has(n.es, E1)) - @@allow('all', hasSome(n.es, [E1])) - @@allow('all', hasEvery(n.es, [E1])) - @@allow('all', isEmpty(n.es)) - } - `); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - s String - @@allow('all', contains(s)) - } - `) - ).toContain('missing argument for parameter "search"'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - s String - @@allow('all', contains('a', s)) - } - `) - ).toContain('first argument must be a field reference'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - s String - s1 String - @@allow('all', contains(s, s1)) - } - `) - ).toContain( - 'second argument must be a literal, an enum, an expression starting with `auth().`, or an array of them' - ); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - i Int - @@allow('all', contains(i, 1)) - } - `) - ).toContain('argument is not assignable to parameter'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - i Int - @@allow('all', i in 1) - } - `) - ).toContain('right operand of "in" must be an array'); - - expect( - await loadModelWithError(` - ${prelude} - model N { - id String @id - m M @relation(fields: [mId], references: [id]) - mId String - } - model M { - id String @id - n N? - @@allow('all', n in [1]) - } - `) - ).toContain('left operand of "in" must be of scalar type'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int - @@allow('all', has(x, 1)) - } - `) - ).toContain('argument is not assignable to parameter'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int[] - @@allow('all', hasSome(x, 1)) - } - `) - ).toContain('argument is not assignable to parameter'); - }); - - it('validator filter function check', async () => { - await loadModel(` - ${prelude} - enum E { - E1 - E2 - } - - model N { - id String @id - e E - es E[] - s String - i Int - m M @relation(fields: [mId], references: [id]) - mId String @unique - } - - model M { - id String @id - s String - e E - es E[] - n N? - - @@validate(e in [E1, E2]) - @@validate(contains(s, 'a')) - @@validate(contains(s, 'a', true)) - @@validate(startsWith(s, 'a')) - @@validate(endsWith(s, 'a')) - @@validate(has(es, E1)) - @@validate(hasSome(es, [E1])) - @@validate(hasEvery(es, [E1])) - @@validate(isEmpty(es)) - } - `); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - s String - @@validate(contains(s)) - } - `) - ).toContain('missing argument for parameter "search"'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - s String - @@validate(contains('a', s)) - } - `) - ).toContain('first argument must be a field reference'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - s String - s1 String - @@validate(contains(s, s1)) - } - `) - ).toContain( - 'second argument must be a literal, an enum, an expression starting with `auth().`, or an array of them' - ); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - i Int - @@validate(contains(i, 1)) - } - `) - ).toContain('argument is not assignable to parameter'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - i Int - @@validate(i in 1) - } - `) - ).toContain('right operand of "in" must be an array'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int - @@validate(has(x, 1)) - } - `) - ).toContain('argument is not assignable to parameter'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int[] - @@validate(hasSome(x, 1)) - } - `) - ).toContain('argument is not assignable to parameter'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - n N? - @@validate(n.value > 0) - } - - model N { - id String @id - value Int - m M @relation(fields: [mId], references: [id]) - mId String @unique - } - `) - ).toContain('`@@validate` condition cannot use relation fields'); - }); - - it('auth function check', async () => { - await loadModel(` - ${prelude} - - model User { - id String @id - name String - } - model B { - id String @id - userId String @default(auth().id) - userName String @default(auth().name) - } - `); - - expect( - await loadModelWithError(` - ${prelude} - - model Post { - id String @id - @@allow('all', auth() != null) - } - `) - ).toContain(`auth() cannot be resolved because no model marked with "@@auth()" or named "User" is found`); - - await loadModel(` - ${prelude} - - model User { - id String @id - name String - } - - model Post { - id String @id - @@allow('all', auth().name != null) - } - `); - - expect( - await loadModelWithError(` - ${prelude} - - model User { - id String @id - name String - } - - model Post { - id String @id - @@allow('all', auth().email != null) - } - `) - ).toContain(`Could not resolve reference to MemberAccessTarget named 'email'.`); - }); - - it('collection predicate expression check', async () => { - expect( - await loadModelWithError(` - ${prelude} - - model A { - id String @id - x Int - b B @relation(references: [id], fields: [bId]) - bId String @unique - } - - model B { - id String @id - a A? - aId String @unique - @@allow('all', a?[x > 0]) - } - `) - ).toContain(`collection predicate can only be used on an array of model type`); - - await loadModel(` - ${prelude} - - model A { - id String @id - x Int - b B @relation(references: [id], fields: [bId]) - bId String - } - - model B { - id String @id - a A[] - @@allow('all', a?[x > 0]) - } - `); - }); - - it('invalid attribute target field', async () => { - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id @gt(10) - } - `) - ).toContain('attribute "@gt" cannot be used on this type of field'); - - expect( - await loadModelWithError(` - ${prelude} - model A { - id String @id - x Int @length(5) - } - `) - ).toContain('attribute "@length" cannot be used on this type of field'); - }); - - it('enum as default', async () => { - await loadModel(` - ${prelude} - - enum E { - E1 - E2 - } - - model M { - id String @id - e E @default(E1) - } - `); - }); - - it('incorrect function expression context', async () => { - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - @@allow('all', autoincrement() > 0) - } - `) - ).toContain('function "autoincrement" is not allowed in the current context: AccessPolicy'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - @@deny('all', uuid() == null) - } - `) - ).toContain('function "uuid" is not allowed in the current context: AccessPolicy'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x String - @@validate(search(x, 'abc')) - } - `) - ).toContain('function "search" is not allowed in the current context: ValidationRule'); - }); - - it('invalid policy rule kind', async () => { - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int - @@allow('read,foo', x > 0) - } - `) - ).toContain('Invalid policy rule kind: "foo", allowed: "create", "read", "update", "delete", "all"'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int - @@deny('update,foo', x > 0) - } - `) - ).toContain('Invalid policy rule kind: "foo", allowed: "create", "read", "update", "delete", "all"'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int @allow('foo', x > 0) - } - `) - ).toContain('Invalid policy rule kind: "foo", allowed: "read", "update", "all"'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int @deny('foo', x < 0) - } - `) - ).toContain('Invalid policy rule kind: "foo", allowed: "read", "update", "all"'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - x Int @allow('update', future().x > 0) - } - `) - ).toContain('"future()" is not allowed in field-level policy rules'); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - n N @allow('update', n.x > 0) - } - - model N { - id String @id - x Int - m M? @relation(fields: [mId], references: [id]) - mId String - } - `) - ).toContain( - 'Field-level policy rules with "update" or "all" kind are not allowed for relation fields. Put rules on foreign-key fields instead.' - ); - - expect( - await loadModelWithError(` - ${prelude} - model M { - id String @id - n N[] @allow('update', n.x > 0) - } - - model N { - id String @id - x Int - m M? @relation(fields: [mId], references: [id]) - mId String - } - `) - ).toContain( - 'Field-level policy rules with "update" or "all" kind are not allowed for relation fields. Put rules on foreign-key fields instead.' - ); - }); - - it('type def field attribute', async () => { - await expect( - loadModelWithError(` - model User { - id String @id - profile Profile - } - - type Profile { - email String @omit - } - `) - ).resolves.toContain(`attribute "@omit" cannot be used on type declaration fields`); - }); - - it('validates regex', async () => { - await expect( - loadModelWithError(` - ${prelude} - model User { - id String @id - phone String @regex(id) - } - `) - ).resolves.toContain('Expecting a string literal'); - - await expect( - loadModelWithError(` - ${prelude} - model User { - id String @id - phone String @regex("^(\\+46|0)[0-9]{7,12}$") - } - `) - ).resolves.toContain('Invalid regular expression'); - }); -}); diff --git a/packages/schema/tests/schema/validation/cyclic-inheritance.test.ts b/packages/schema/tests/schema/validation/cyclic-inheritance.test.ts deleted file mode 100644 index 494dad2be..000000000 --- a/packages/schema/tests/schema/validation/cyclic-inheritance.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { loadModelWithError } from '../../utils'; - -describe('Cyclic inheritance', () => { - it('abstract inheritance', async () => { - const errors = await loadModelWithError( - ` - abstract model A extends B {} - abstract model B extends A {} - model C extends B { - id Int @id - } - ` - ); - expect(errors).toContain('Circular inheritance detected: A -> B -> A'); - expect(errors).toContain('Circular inheritance detected: B -> A -> B'); - expect(errors).toContain('Circular inheritance detected: C -> B -> A -> B'); - }); - - it('delegate inheritance', async () => { - const errors = await loadModelWithError( - ` - model A extends B { - typeA String - @@delegate(typeA) - } - model B extends A { - typeB String - @@delegate(typeB) - } - model C extends B { - id Int @id - } - ` - ); - expect(errors).toContain('Circular inheritance detected: A -> B -> A'); - expect(errors).toContain('Circular inheritance detected: B -> A -> B'); - expect(errors).toContain('Circular inheritance detected: C -> B -> A -> B'); - }); -}); diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts deleted file mode 100644 index fd4274eab..000000000 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ /dev/null @@ -1,784 +0,0 @@ -import { loadModel, safelyLoadModel, errorLike } from '../../utils'; - -describe('Data Model Validation Tests', () => { - const prelude = ` - datasource db { - provider = "postgresql" - url = "url" - } - `; - - it('duplicated fields', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - id String @id - x Int - x String - } - `); - - expect(result).toMatchObject(errorLike('Duplicated declaration name "x"')); - }); - - it('scalar types', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - id String @id - a String - b Boolean? - c Int[] @default([]) - c1 Int[] @default([1, 2, 3]) - d BigInt - e Float - f Decimal - g DateTime - h Json - i Bytes - } - `); - expect(result).toMatchObject({ status: 'fulfilled' }); - }); - - it('Unsupported type valid arg', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - id String @id - a Unsupported('foo') - } - `); - - expect(result).toMatchObject({ status: 'fulfilled' }); - }); - - it('Unsupported type invalid arg', async () => { - expect( - await safelyLoadModel(` - ${prelude} - model M { - id String @id - a Unsupported(123) - } - `) - ).toMatchObject(errorLike('Unsupported type argument must be a string literal')); - }); - - it('Unsupported type used in expression', async () => { - expect( - await safelyLoadModel(` - ${prelude} - model M { - id String @id - a Unsupported('a') - @@allow('all', a == 'a') - } - `) - ).toMatchObject(errorLike('incompatible operand types')); - }); - - it('Using `this` in collection predicate', async () => { - expect( - await safelyLoadModel(` - ${prelude} - model User { - id String @id - members User[] - @@allow('all', members?[this == auth()]) - } - `) - ).toBeTruthy(); - - expect( - await loadModel(` - model User { - id String @id - members User[] - @@allow('all', members?[id == auth().id]) - } - `) - ).toBeTruthy(); - }); - - it('mix array and optional', async () => { - expect( - await safelyLoadModel(` - ${prelude} - model M { - id String @id - x Int[]? - } - `) - ).toMatchObject(errorLike('Optional lists are not supported. Use either `Type[]` or `Type?`')); - }); - - it('unresolved field type', async () => { - expect( - await safelyLoadModel(` - ${prelude} - model M { - id String @id - x Integer - } - `) - ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); - - expect( - await safelyLoadModel(` - ${prelude} - model M { - id String @id - x Integer[] - } - `) - ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); - - expect( - await safelyLoadModel(` - ${prelude} - model M { - id String @id - x Integer? - } - `) - ).toMatchObject(errorLike(`Could not resolve reference to TypeDeclaration named 'Integer'.`)); - }); - - describe('id field', () => { - const err = - 'Model must have at least one unique criteria. Either mark a single field with `@id`, `@unique` or add a multi field criterion with `@@id([])` or `@@unique([])` to the model.'; - - it('should error when there are no unique fields', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int - @@allow('all', x > 0) - } - `); - expect(result).toMatchObject(errorLike(err)); - }); - - it('should should use @unique when there is no @id', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - id Int @unique - x Int - @@allow('all', x > 0) - } - `); - expect(result).toMatchObject({ status: 'fulfilled' }); - }); - - // @@unique used as id - it('should suceed when @@unique used as id', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int - @@unique([x]) - @@allow('all', x > 0) - } - `); - expect(result).toMatchObject({ status: 'fulfilled' }); - }); - - it('should succeed when @id is an enum type', async () => { - const result = await safelyLoadModel(` - ${prelude} - enum E { - A - B - } - model M { - id E @id - } - `); - expect(result).toMatchObject({ status: 'fulfilled' }); - }); - - it('should succeed when @@id is an enum type', async () => { - const result = await safelyLoadModel(` - ${prelude} - enum E { - A - B - } - model M { - x Int - y E - @@id([x, y]) - } - `); - expect(result).toMatchObject({ status: 'fulfilled' }); - }); - - it('should error when there are no id fields, even when denying access', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int - @@deny('all', x <= 0) - } - `); - - expect(result).toMatchObject(errorLike(err)); - }); - - it('should error when there are not id fields, without access restrictions', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int @gt(0) - } - `); - - expect(result).toMatchObject(errorLike(err)); - }); - - it('should error when there is more than one field marked as @id', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int @id - y Int @id - } - `); - expect(result).toMatchObject(errorLike(`Model can include at most one field with @id attribute`)); - }); - - it('should error when both @id and @@id are used', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int @id - y Int - @@id([x, y]) - } - `); - expect(result).toMatchObject( - errorLike(`Model cannot have both field-level @id and model-level @@id attributes`) - ); - }); - - it('should error when @id used on optional field', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int? @id - } - `); - expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)); - }); - - it('should error when @@id used on optional field', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int? - @@id([x]) - } - `); - expect(result).toMatchObject(errorLike(`Field with @id attribute must not be optional`)); - }); - - it('should error when @id used on list field', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int[] @id - } - `); - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); - }); - - it('should error when @@id used on list field', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Int[] - @@id([x]) - } - `); - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); - }); - - it('should error when @id used on a Json field', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Json @id - } - `); - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); - }); - - it('should error when @@id used on a Json field', async () => { - const result = await safelyLoadModel(` - ${prelude} - model M { - x Json - @@id([x]) - } - `); - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); - }); - - it('should error when @id used on a reference field', async () => { - const result = await safelyLoadModel(` - ${prelude} - model Id { - id String @id - } - model M { - myId Id @id - } - `); - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); - }); - - it('should error when @@id used on a reference field', async () => { - const result = await safelyLoadModel(` - ${prelude} - model Id { - id String @id - } - model M { - myId Id - @@id([myId]) - } - `); - expect(result).toMatchObject(errorLike(`Field with @id attribute must be of scalar or enum type`)); - }); - }); - - it('relation', async () => { - // one-to-one - await loadModel(` - ${prelude} - model A { - id String @id - b B? - } - - model B { - id String @id - a A @relation(fields: [foreignId], references: [id], onUpdate: Cascade, onDelete: Cascade) - foreignId String @unique - } - `); - - // one-to-many - await loadModel(` - ${prelude} - model A { - id String @id - b B[] - } - - model B { - id String @id - a A @relation(fields: [foreignId], references: [id]) - foreignId String - } - `); - - // many-to-many implicit - //https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations#implicit-many-to-many-relations - await loadModel(` - ${prelude} - model Post { - id Int @id @default(autoincrement()) - title String - categories Category[] - } - - model Category { - id Int @id @default(autoincrement()) - name String - posts Post[] - } - `); - - // one-to-one incomplete - expect( - await safelyLoadModel(` - ${prelude} - model A { - id String @id - b B? - } - - model B { - id String @id - } - `) - ).toMatchObject( - errorLike(`The relation field "b" on model "A" is missing an opposite relation field on model "B"`) - ); - - // one-to-one ambiguous - expect( - await safelyLoadModel(` - ${prelude} - model A { - id String @id - b B? - } - - model B { - id String @id - a A - a1 A - } - `) - ).toMatchObject(errorLike(`Fields "a", "a1" on model "B" refer to the same relation to model "A"`)); - - // fields or references missing - expect( - await safelyLoadModel(` - ${prelude} - model A { - id String @id - b B? - } - - model B { - id String @id - a A @relation(fields: [aId]) - aId String - } - `) - ).toMatchObject(errorLike(`"fields" and "references" must be provided together`)); - - // one-to-one inconsistent attribute - expect( - await safelyLoadModel(` - ${prelude} - model A { - id String @id - b B? @relation(references: [id]) - } - - model B { - id String @id - a A @relation(fields: [aId], references: [id]) - aId String - } - `) - ).toMatchObject(errorLike(`"fields" and "references" must be provided only on one side of relation field`)); - - // references mismatch - expect( - await safelyLoadModel(` - ${prelude} - model A { - myId Int @id - b B? - } - - model B { - id String @id - a A @relation(fields: [aId], references: [id]) - aId String @unique - } - `) - ).toMatchObject(errorLike(`values of "references" and "fields" must have the same type`)); - - // "fields" and "references" typing consistency - expect( - await safelyLoadModel(` - ${prelude} - model A { - id Int @id - b B? - } - - model B { - id String @id - a A @relation(fields: [aId], references: [id]) - aId String @unique - } - `) - ).toMatchObject(errorLike(`values of "references" and "fields" must have the same type`)); - - // one-to-one missing @unique - expect( - await safelyLoadModel(` - ${prelude} - model A { - id String @id - b B? - } - - model B { - id String @id - a A @relation(fields: [aId], references: [id]) - aId String - } - `) - ).toMatchObject( - errorLike( - `Field "aId" on model "B" is part of a one-to-one relation and must be marked as @unique or be part of a model-level @@unique attribute` - ) - ); - - // missing @relation - expect( - await safelyLoadModel(` - ${prelude} - model A { - id String @id - b B? - } - - model B { - id String @id - a A - } - `) - ).toMatchObject( - errorLike( - `Field for one side of relation must carry @relation attribute with both "fields" and "references"` - ) - ); - - // wrong relation owner field type - expect( - await safelyLoadModel(` - ${prelude} - model A { - id String @id - b B - } - - model B { - id String @id - a A @relation(fields: [aId], references: [id]) - aId String - } - `) - ).toMatchObject(errorLike(`Relation field needs to be list or optional`)); - - // unresolved field - expect( - await safelyLoadModel(` - ${prelude} - model A { - id String @id - b B? - } - - model B { - id String @id - a A @relation(fields: [aId], references: [id]) - } - `) - ).toMatchObject(errorLike(`Could not resolve reference to ReferenceTarget named 'aId'.`)); - - // enum as foreign key - await loadModel(` - ${prelude} - - enum Role { - ADMIN - USER - } - - model A { - id String @id - role Role @unique - bs B[] - } - - model B { - id String @id - a A @relation(fields: [aRole], references: [role]) - aRole Role - } - `); - }); - - it('self relation', async () => { - // one-to-one - // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#one-to-one-self-relations - await loadModel(` - ${prelude} - model User { - id Int @id @default(autoincrement()) - name String? - successorId Int? @unique - successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id]) - predecessor User? @relation("BlogOwnerHistory") - } - `); - - // one-to-many - // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#one-to-many-self-relations - await loadModel(` - ${prelude} - model User { - id Int @id @default(autoincrement()) - name String? - teacherId Int? - teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id]) - students User[] @relation("TeacherStudents") - } - `); - - // many-to-many - // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#many-to-many-self-relations - await loadModel(` - ${prelude} - model User { - id Int @id @default(autoincrement()) - name String? - followedBy User[] @relation("UserFollows") - following User[] @relation("UserFollows") - } - `); - - // many-to-many explicit - // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#many-to-many-self-relations - await loadModel(` - ${prelude} - model User { - id Int @id @default(autoincrement()) - name String? - followedBy Follows[] @relation("following") - following Follows[] @relation("follower") - } - - model Follows { - follower User @relation("follower", fields: [followerId], references: [id]) - followerId Int - following User @relation("following", fields: [followingId], references: [id]) - followingId Int - - @@id([followerId, followingId]) - } - `); - - await loadModel(` - ${prelude} - model User { - id Int @id - eventTypes EventType[] @relation("user_eventtype") - } - - model EventType { - id Int @id - users User[] @relation("user_eventtype") - } - `); - - // multiple self relations - // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations#defining-multiple-self-relations-on-the-same-model - await loadModel(` - ${prelude} - model User { - id Int @id @default(autoincrement()) - name String? - teacherId Int? - teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id]) - students User[] @relation("TeacherStudents") - followedBy User[] @relation("UserFollows") - following User[] @relation("UserFollows") - } - `); - }); - - it('abstract base type', async () => { - const errors = await safelyLoadModel(` - ${prelude} - - abstract model Base { - id String @id - } - - model A { - a String - } - - model B extends Base,A { - b String - } - `); - - expect(errors).toMatchObject( - errorLike(`Model A cannot be extended because it's neither abstract nor marked as "@@delegate"`) - ); - - // relation incomplete from multiple level inheritance - expect( - await safelyLoadModel(` - ${prelude} - model User { - id Int @id @default(autoincrement()) - } - - abstract model Base { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - } - - abstract model Base1 extends Base { - isPublic Boolean @default(false) - } - - model A extends Base1 { - a String - } - `) - ).toMatchObject( - errorLike(`The relation field "user" on model "A" is missing an opposite relation field on model "User"`) - ); - - // one-to-one relation field and @@unique defined in different models - await loadModel(` - ${prelude} - - abstract model Base { - id String @id - a A @relation(fields: [aId], references: [id]) - aId String - } - - model A { - id String @id - b B? - } - - model B extends Base{ - @@unique([aId]) - } - `); - }); - - it('delegate base type', async () => { - const errors = await safelyLoadModel(` - ${prelude} - - model Base1 { - id String @id - type String - @@delegate(type) - } - - model Base2 { - id String @id - type String - @@delegate(type) - } - - model A extends Base1,Base2 { - a String - } - `); - - expect(errors).toMatchObject(errorLike(`Extending from multiple delegate models is not supported`)); - }); -}); diff --git a/packages/schema/tests/schema/validation/datasource-validation.test.ts b/packages/schema/tests/schema/validation/datasource-validation.test.ts deleted file mode 100644 index f73e5a661..000000000 --- a/packages/schema/tests/schema/validation/datasource-validation.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { loadModel, loadModelWithError, safelyLoadModel } from '../../utils'; - -describe('Datasource Validation Tests', () => { - it('missing fields', async () => { - const result = await safelyLoadModel(` - datasource db { - } - `); - - expect(result).toMatchObject({ - status: 'rejected', - reason: { - cause: [{ message: 'datasource must include a "provider" field' }], - }, - }); - }); - - it('dup fields', async () => { - expect( - await loadModelWithError(` - datasource db { - provider = 'abc' - provider = 'abc' - } - `) - ).toContain('Duplicated declaration name "provider"'); - }); - - it('invalid provider value', async () => { - expect( - await loadModelWithError(` - datasource db { - provider = 123 - } - `) - ).toContain('"provider" must be set to a string literal'); - - expect( - await loadModelWithError(` - datasource db { - provider = 'abc' - } - `) - ).toContain('Provider "abc" is not supported'); - }); - - it('invalid url value', async () => { - expect( - await loadModelWithError(` - datasource db { - url = 123 - } - `) - ).toContain('"url" must be set to a string literal or an invocation of "env" function'); - - expect( - await loadModelWithError(` - datasource db { - url = uuid() - } - `) - ).toContain('"url" must be set to a string literal or an invocation of "env" function'); - }); - - it('invalid relationMode value', async () => { - expect( - await loadModelWithError(` - datasource db { - relationMode = 123 - } - `) - ).toContain('"relationMode" must be set to "foreignKeys" or "prisma"'); - - expect( - await loadModelWithError(` - datasource db { - relationMode = "foo" - } - `) - ).toContain('"relationMode" must be set to "foreignKeys" or "prisma"'); - }); - - it('success', async () => { - await loadModel(` - datasource db { - provider = "postgresql" - url = "url" - relationMode = "prisma" - } - `); - - await loadModel(` - datasource db { - provider = "postgresql" - url = env("url") - relationMode = "foreignKeys" - } - `); - }); -}); diff --git a/packages/schema/tests/schema/validation/enum-validation.test.ts b/packages/schema/tests/schema/validation/enum-validation.test.ts deleted file mode 100644 index fc31a0c92..000000000 --- a/packages/schema/tests/schema/validation/enum-validation.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { loadModelWithError } from '../../utils'; - -describe('Enum Validation Tests', () => { - const prelude = ` - datasource db { - provider = "postgresql" - url = "url" - } - `; - - it('duplicated fields', async () => { - expect( - await loadModelWithError(` - ${prelude} - enum E { - A - A - } - `) - ).toContain('Duplicated declaration name "A"'); - }); -}); diff --git a/packages/schema/tests/schema/validation/schema-validation.test.ts b/packages/schema/tests/schema/validation/schema-validation.test.ts deleted file mode 100644 index ca0efa697..000000000 --- a/packages/schema/tests/schema/validation/schema-validation.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { loadModelWithError } from '../../utils'; - -describe('Toplevel Schema Validation Tests', () => { - it('too many datasources', async () => { - expect( - await loadModelWithError(` - datasource db1 { - provider = 'postgresql' - url = env('DATABASE_URL') - } - datasource db2 { - provider = 'postgresql' - url = env('DATABASE_URL') - } - `) - ).toContain('Multiple datasource declarations are not allowed'); - }); - - it('duplicated declaration names', async () => { - expect( - await loadModelWithError(` - model X {id String @id } - model X { id String @id } - `) - ).toContain('Duplicated declaration name "X"'); - }); - - it('not exsited import', async () => { - expect( - await loadModelWithError(` - import 'models/abc' - datasource db1 { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - model X {id String @id } - `) - ).toContain('Cannot find model file models/abc.zmodel'); - }); - - it('not existing import with extension', async () => { - expect( - await loadModelWithError(` - import 'models/abc.zmodel' - datasource db1 { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - model X {id String @id } - `) - ).toContain('Cannot find model file models/abc.zmodel'); - }) - - it('multiple auth models', async () => { - expect( - await loadModelWithError(` - datasource db1 { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - model X { - id String @id - @@auth - } - - model Y { - id String @id - @@auth - } - `) - ).toContain('Multiple `@@auth` models are not allowed'); - }); -}); diff --git a/packages/schema/tests/schema/zmodel-generator.test.ts b/packages/schema/tests/schema/zmodel-generator.test.ts deleted file mode 100644 index d3bee36dd..000000000 --- a/packages/schema/tests/schema/zmodel-generator.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -/// - -import { ZModelCodeGenerator } from '@zenstackhq/sdk'; -import { DataModel, DataModelAttribute, DataModelFieldAttribute } from '@zenstackhq/sdk/ast'; -import fs from 'fs'; -import path from 'path'; -import { loadModel } from '../utils'; - -describe('ZModel Generator Tests', () => { - const generator = new ZModelCodeGenerator(); - - it('run generator', async () => { - const content = fs.readFileSync(path.join(__dirname, './all-features.zmodel'), 'utf-8'); - const model = await loadModel(content, true, false, false); - const generated = generator.generate(model); - // fs.writeFileSync(path.join(__dirname, './all-features-baseline.zmodel'), generated, 'utf-8'); - await loadModel(generated); - }); - - function checkAttribute(ast: DataModelAttribute | DataModelFieldAttribute, expected: string) { - const result = generator.generate(ast); - expect(result).toBe(expected); - } - - async function getModule(schema: string) { - if (!schema.includes('datasource ')) { - schema = - ` - datasource db { - provider = 'postgresql' - url = 'dummy' - } - ` + schema; - } - - return loadModel(schema); - } - - async function getModelDeclaration(schema: string, name: string) { - const module = await getModule(schema); - return module.declarations.find((d) => d.name === name) as DataModel; - } - - it('check field attribute', async () => { - const model = await getModelDeclaration( - ` - model Test{ - id String @id @length(4, 50) @regex('^[0-9a-zA-Z]{4,16}$') - } - `, - 'Test' - ); - - checkAttribute(model.fields[0].attributes[0], '@id'); - checkAttribute(model.fields[0].attributes[1], '@length(4, 50)'); - checkAttribute(model.fields[0].attributes[2], "@regex('^[0-9a-zA-Z]{4,16}$')"); - }); - - it('check basic model attribute', async () => { - const model = await getModelDeclaration( - ` - model User { - id String @id - - @@deny('all', auth() == null) - @@allow('create', true) - } - `, - 'User' - ); - - checkAttribute(model.attributes[0], `@@deny('all', auth() == null)`); - checkAttribute(model.attributes[1], `@@allow('create', true)`); - }); - - it('check collection expression', async () => { - const model = await getModelDeclaration( - ` - enum UserRole { - USER - ADMIN - } - - model User { - id String @id - name String - role UserRole - deleted Boolean - - posts Post[] - - @@allow('read', posts ? [author == auth()]) - - @@deny('read', name == '123' && (role == USER || name == '456')) - - @@allow('delete', posts?[author == auth() && ( length <10 || author.role == USER) && !author.deleted]) - } - - model Post { - id String @id - author User? @relation(fields: [authorId], references: [id]) - authorId String? - length Int - } - `, - 'User' - ); - - checkAttribute(model.attributes[0], `@@allow('read', posts?[author == auth()])`); - checkAttribute(model.attributes[1], `@@deny('read', name == '123' && (role == USER || name == '456'))`); - checkAttribute( - model.attributes[2], - `@@allow('delete', posts?[author == auth() && (length < 10 || author.role == USER) && !author.deleted])` - ); - }); -}); diff --git a/packages/schema/tests/utils.ts b/packages/schema/tests/utils.ts deleted file mode 100644 index e72373f82..000000000 --- a/packages/schema/tests/utils.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Model } from '@zenstackhq/sdk/ast'; -import * as fs from 'fs'; -import { NodeFileSystem } from 'langium/node'; -import * as path from 'path'; -import * as tmp from 'tmp'; -import { URI } from 'vscode-uri'; -import { createZModelServices } from '../src/language-server/zmodel-module'; -import { mergeBaseModels } from '../src/utils/ast-utils'; - -tmp.setGracefulCleanup(); - -type Errorish = Error | { message: string; stack?: string } | string; - -export class SchemaLoadingError extends Error { - cause: Errors; - constructor(public readonly errors: Errors) { - const stack = errors.find( - (e): e is typeof e & { stack: string } => typeof e === 'object' && 'stack' in e - )?.stack; - const message = errors.map((e) => (typeof e === 'string' ? e : e.message)).join('\n'); - - super(`Schema error:\n${message}`); - - if (stack) { - const shiftedStack = stack.split('\n').slice(1).join('\n'); - this.stack = shiftedStack; - } - this.cause = errors; - } -} - -export async function loadModel(content: string, validate = true, verbose = true, mergeBase = true) { - const { name: docPath } = tmp.fileSync({ postfix: '.zmodel' }); - fs.writeFileSync(docPath, content); - const { shared, ZModel } = createZModelServices(NodeFileSystem); - const stdLib = shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file(path.resolve(__dirname, '../../schema/src/res/stdlib.zmodel')) - ); - const doc = shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(docPath)); - - if (doc.parseResult.lexerErrors.length > 0) { - throw new SchemaLoadingError(doc.parseResult.lexerErrors); - } - - if (doc.parseResult.parserErrors.length > 0) { - throw new SchemaLoadingError(doc.parseResult.parserErrors); - } - - await shared.workspace.DocumentBuilder.build([stdLib, doc], { - validationChecks: validate ? 'all' : 'none', - }); - - const validationErrors = (doc.diagnostics ?? []).filter((e) => e.severity === 1); - if (validationErrors.length > 0) { - for (const validationError of validationErrors) { - if (verbose) { - const range = doc.textDocument.getText(validationError.range); - console.error( - `line ${validationError.range.start.line + 1}: ${validationError.message}${ - range ? ' [' + range + ']' : '' - }` - ); - } - } - throw new SchemaLoadingError(validationErrors); - } - - const model = (await doc.parseResult.value) as Model; - - if (mergeBase) { - mergeBaseModels(model, ZModel.references.Linker); - } - - return model; -} - -export async function loadModelWithError(content: string, verbose = false) { - try { - await loadModel(content, true, verbose); - } catch (err) { - if (!(err instanceof SchemaLoadingError)) { - throw err; - } - return (err as SchemaLoadingError).message; - } - throw new Error('No error is thrown'); -} - -export async function safelyLoadModel(content: string, validate = true, verbose = false) { - const [result] = await Promise.allSettled([loadModel(content, validate, verbose)]); - - return result; -} - -export const errorLike = (msg: string) => ({ - reason: { - message: expect.stringContaining(msg), - }, -}); diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/sdk/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/sdk/LICENSE b/packages/sdk/LICENSE deleted file mode 120000 index 30cff7403..000000000 --- a/packages/sdk/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/sdk/README.md b/packages/sdk/README.md deleted file mode 100644 index 63f7fa833..000000000 --- a/packages/sdk/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ZenStack plugin development SDK - -This package provides types and utilities for developing a ZenStack plugin. - -Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/sdk/src/code-gen.ts b/packages/sdk/src/code-gen.ts deleted file mode 100644 index be8d6a7f4..000000000 --- a/packages/sdk/src/code-gen.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { CompilerOptions, DiagnosticCategory, ModuleKind, Project, ScriptTarget, SourceFile } from 'ts-morph'; -import { PluginError } from './types'; - -/** - * Creates a TS code generation project - */ -export function createProject(options?: CompilerOptions) { - return new Project({ - compilerOptions: { - target: ScriptTarget.ES2016, - module: ModuleKind.CommonJS, - esModuleInterop: true, - declaration: true, - strict: true, - skipLibCheck: true, - noEmitOnError: true, - noImplicitAny: false, - skipDefaultLibCheck: true, - types: ['node'], - ...options, - }, - }); -} - -export function saveSourceFile(sourceFile: SourceFile) { - sourceFile.replaceWithText( - `/****************************************************************************** - * This file was generated by ZenStack CLI. - ******************************************************************************/ - -/* eslint-disable */ - - ${sourceFile.getText()}` - ); - sourceFile.formatText(); - sourceFile.saveSync(); -} - -/** - * Persists a TS project to disk. - */ -export async function saveProject(project: Project) { - project.getSourceFiles().forEach(saveSourceFile); - await project.save(); -} - -/** - * Emit a TS project to JS files. - */ -export async function emitProject(project: Project) { - const errors = project.getPreEmitDiagnostics().filter((d) => d.getCategory() === DiagnosticCategory.Error); - if (errors.length > 0) { - console.error('Error compiling generated code:'); - console.error(project.formatDiagnosticsWithColorAndContext(errors.slice(0, 10))); - await project.save(); - throw new PluginError('', `Error compiling generated code`); - } - - const result = await project.emit(); - - const emitErrors = result.getDiagnostics().filter((d) => d.getCategory() === DiagnosticCategory.Error); - if (emitErrors.length > 0) { - console.error('Some generated code is not emitted:'); - console.error(project.formatDiagnosticsWithColorAndContext(emitErrors.slice(0, 10))); - await project.save(); - throw new PluginError('', `Error emitting generated code`); - } -} - -/* - * Abstraction for source code writer. - */ -export interface CodeWriter { - block(callback: () => void): void; - inlineBlock(callback: () => void): void; - write(text: string): void; - writeLine(text: string): void; - conditionalWrite(condition: boolean, text: string): void; -} - -/** - * A fast code writer. - */ -export class FastWriter implements CodeWriter { - private content = ''; - private indentLevel = 0; - - constructor(private readonly indentSize = 4) {} - - get result() { - return this.content; - } - - block(callback: () => void) { - this.content += '{\n'; - this.indentLevel++; - callback(); - this.indentLevel--; - this.content += '\n}'; - } - - inlineBlock(callback: () => void) { - this.content += '{'; - callback(); - this.content += '}'; - } - - write(text: string) { - this.content += this.indent(text); - } - - writeLine(text: string) { - this.content += `${this.indent(text)}\n`; - } - - conditionalWrite(condition: boolean, text: string) { - if (condition) { - this.write(text); - } - } - - private indent(text: string) { - return ' '.repeat(this.indentLevel * this.indentSize) + text; - } -} diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/constants.ts deleted file mode 100644 index 1e0d22d67..000000000 --- a/packages/sdk/src/constants.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @zenstackhq/runtime package name - */ -export const RUNTIME_PACKAGE = '@zenstackhq/runtime'; - -export { CrudFailureReason } from '@zenstackhq/runtime'; - -/** - * Expression context - */ -export enum ExpressionContext { - DefaultValue = 'DefaultValue', - AccessPolicy = 'AccessPolicy', - ValidationRule = 'ValidationRule', - Index = 'Index', -} - -export const STD_LIB_MODULE_NAME = 'stdlib.zmodel'; diff --git a/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts b/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts deleted file mode 100644 index 8ec07fe87..000000000 --- a/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { upperCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import type { DMMF } from '../prisma'; -import { AggregateOperationSupport } from './types'; - -const isAggregateOutputType = (name: string) => /(?:Count|Avg|Sum|Min|Max)AggregateOutputType$/.test(name); - -export const isAggregateInputType = (name: string) => - name.endsWith('CountAggregateInput') || - name.endsWith('SumAggregateInput') || - name.endsWith('AvgAggregateInput') || - name.endsWith('MinAggregateInput') || - name.endsWith('MaxAggregateInput'); - -export function addMissingInputObjectTypesForAggregate( - inputObjectTypes: DMMF.InputType[], - outputObjectTypes: DMMF.OutputType[] -) { - const aggregateOutputTypes = outputObjectTypes.filter(({ name }) => isAggregateOutputType(name)); - for (const aggregateOutputType of aggregateOutputTypes) { - const name = aggregateOutputType.name.replace(/(?:OutputType|Output)$/, ''); - inputObjectTypes.push({ - constraints: { maxNumFields: null, minNumFields: null }, - name: `${upperCaseFirst(name)}Input`, - fields: aggregateOutputType.fields.map((field) => ({ - name: field.name, - isNullable: false, - isRequired: false, - inputTypes: [ - { - isList: false, - type: 'True', - location: 'scalar', - }, - ], - })), - }); - } -} - -export function resolveAggregateOperationSupport(inputObjectTypes: DMMF.InputType[]) { - const aggregateOperationSupport: AggregateOperationSupport = {}; - for (const inputType of inputObjectTypes) { - if (isAggregateInputType(inputType.name)) { - const name = inputType.name.replace('AggregateInput', ''); - if (name.endsWith('Count')) { - const model = name.replace('Count', ''); - aggregateOperationSupport[model] = { - ...aggregateOperationSupport[model], - count: true, - }; - } else if (name.endsWith('Min')) { - const model = name.replace('Min', ''); - aggregateOperationSupport[model] = { - ...aggregateOperationSupport[model], - min: true, - }; - } else if (name.endsWith('Max')) { - const model = name.replace('Max', ''); - aggregateOperationSupport[model] = { - ...aggregateOperationSupport[model], - max: true, - }; - } else if (name.endsWith('Sum')) { - const model = name.replace('Sum', ''); - aggregateOperationSupport[model] = { - ...aggregateOperationSupport[model], - sum: true, - }; - } else if (name.endsWith('Avg')) { - const model = name.replace('Avg', ''); - aggregateOperationSupport[model] = { - ...aggregateOperationSupport[model], - avg: true, - }; - } - } - } - return aggregateOperationSupport; -} diff --git a/packages/sdk/src/dmmf-helpers/include-helpers.ts b/packages/sdk/src/dmmf-helpers/include-helpers.ts deleted file mode 100644 index cb5b3ed3a..000000000 --- a/packages/sdk/src/dmmf-helpers/include-helpers.ts +++ /dev/null @@ -1,94 +0,0 @@ -import semver from 'semver'; -import { getPrismaVersion, type DMMF } from '../prisma'; -import { checkIsModelRelationField, checkModelHasManyModelRelation, checkModelHasModelRelation } from './model-helpers'; - -export function addMissingInputObjectTypesForInclude( - inputObjectTypes: DMMF.InputType[], - models: readonly DMMF.Model[] -) { - // generate input object types necessary to support ModelInclude with relation support - const generatedIncludeInputObjectTypes = generateModelIncludeInputObjectTypes(models); - - for (const includeInputObjectType of generatedIncludeInputObjectTypes) { - inputObjectTypes.push(includeInputObjectType); - } -} -function generateModelIncludeInputObjectTypes(models: readonly DMMF.Model[]) { - const modelIncludeInputObjectTypes: DMMF.InputType[] = []; - const prismaVersion = getPrismaVersion(); - for (const model of models) { - const { name: modelName, fields: modelFields } = model; - const fields: DMMF.SchemaArg[] = []; - - for (const modelField of modelFields) { - const { name: modelFieldName, isList, type } = modelField; - - const isRelationField = checkIsModelRelationField(modelField); - - if (isRelationField) { - const field: DMMF.SchemaArg = { - name: modelFieldName, - isRequired: false, - isNullable: false, - inputTypes: [ - { isList: false, type: 'Boolean', location: 'scalar' }, - { - isList: false, - type: isList - ? `${type}FindManyArgs` - : prismaVersion && semver.gte(prismaVersion, '6.0.0') - ? `${type}DefaultArgs` // Prisma 6+ removed [Model]Args type - : `${type}Args`, - location: 'inputObjectTypes', - namespace: 'prisma', - }, - ], - }; - fields.push(field); - } - } - - /** - * include is not generated for models that do not have a relation with any other models - * -> continue onto the next model - */ - const hasRelationToAnotherModel = checkModelHasModelRelation(model); - if (!hasRelationToAnotherModel) { - continue; - } - - const hasManyRelationToAnotherModel = checkModelHasManyModelRelation(model); - - const shouldAddCountField = hasManyRelationToAnotherModel; - if (shouldAddCountField) { - const inputTypes: DMMF.InputTypeRef[] = [{ isList: false, type: 'Boolean', location: 'scalar' }]; - inputTypes.push({ - isList: false, - type: - prismaVersion && semver.gte(prismaVersion, '6.0.0') - ? `${modelName}CountOutputTypeDefaultArgs` // Prisma 6+ removed [Model]CountOutputTypeArgs type - : `${modelName}CountOutputTypeArgs`, - location: 'inputObjectTypes', - namespace: 'prisma', - }); - const _countField: DMMF.SchemaArg = { - name: '_count', - isRequired: false, - isNullable: false, - inputTypes, - }; - fields.push(_countField); - } - - const modelIncludeInputObjectType: DMMF.InputType = { - name: `${modelName}Include`, - constraints: { - maxNumFields: null, - minNumFields: null, - }, - fields, - }; - modelIncludeInputObjectTypes.push(modelIncludeInputObjectType); - } - return modelIncludeInputObjectTypes; -} diff --git a/packages/sdk/src/dmmf-helpers/index.ts b/packages/sdk/src/dmmf-helpers/index.ts deleted file mode 100644 index 07c5b5a8a..000000000 --- a/packages/sdk/src/dmmf-helpers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './aggregate-helpers'; -export * from './include-helpers'; -export * from './model-helpers'; -export * from './modelArgs-helpers'; -export * from './select-helpers'; -export * from './types'; -export * from './missing-types-helper'; diff --git a/packages/sdk/src/dmmf-helpers/missing-types-helper.ts b/packages/sdk/src/dmmf-helpers/missing-types-helper.ts deleted file mode 100644 index dcdff8684..000000000 --- a/packages/sdk/src/dmmf-helpers/missing-types-helper.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { DMMF } from '../prisma'; -import { addMissingInputObjectTypesForAggregate } from './aggregate-helpers'; -import { addMissingInputObjectTypesForInclude } from './include-helpers'; -import { addMissingInputObjectTypesForModelArgs } from './modelArgs-helpers'; -import { addMissingInputObjectTypesForSelect } from './select-helpers'; - -export function addMissingInputObjectTypes( - inputObjectTypes: DMMF.InputType[], - outputObjectTypes: DMMF.OutputType[], - models: DMMF.Model[] -) { - addMissingInputObjectTypesForAggregate(inputObjectTypes, outputObjectTypes); - addMissingInputObjectTypesForSelect(inputObjectTypes, outputObjectTypes, models); - addMissingInputObjectTypesForModelArgs(inputObjectTypes, models); - addMissingInputObjectTypesForInclude(inputObjectTypes, models); -} diff --git a/packages/sdk/src/dmmf-helpers/model-helpers.ts b/packages/sdk/src/dmmf-helpers/model-helpers.ts deleted file mode 100644 index 2528eeeed..000000000 --- a/packages/sdk/src/dmmf-helpers/model-helpers.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { DMMF } from '../prisma'; - -export function checkModelHasModelRelation(model: DMMF.Model) { - const { fields: modelFields } = model; - for (const modelField of modelFields) { - const isRelationField = checkIsModelRelationField(modelField); - if (isRelationField) { - return true; - } - } - return false; -} - -export function checkModelHasManyModelRelation(model: DMMF.Model) { - const { fields: modelFields } = model; - for (const modelField of modelFields) { - const isManyRelationField = checkIsManyModelRelationField(modelField); - if (isManyRelationField) { - return true; - } - } - return false; -} - -export function checkIsModelRelationField(modelField: DMMF.Field) { - const { kind, relationName } = modelField; - return kind === 'object' && !!relationName; -} - -export function checkIsManyModelRelationField(modelField: DMMF.Field) { - return checkIsModelRelationField(modelField) && modelField.isList; -} - -export function findModelByName(models: readonly DMMF.Model[], modelName: string) { - return models.find(({ name }) => name === modelName); -} diff --git a/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts b/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts deleted file mode 100644 index b55767e92..000000000 --- a/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts +++ /dev/null @@ -1,70 +0,0 @@ -import semver from 'semver'; -import { getPrismaVersion, type DMMF } from '../prisma'; -import { checkModelHasModelRelation } from './model-helpers'; - -export function addMissingInputObjectTypesForModelArgs( - inputObjectTypes: DMMF.InputType[], - models: readonly DMMF.Model[] -) { - const modelArgsInputObjectTypes = generateModelArgsInputObjectTypes(models); - - for (const modelArgsInputObjectType of modelArgsInputObjectTypes) { - inputObjectTypes.push(modelArgsInputObjectType); - } -} -function generateModelArgsInputObjectTypes(models: readonly DMMF.Model[]) { - const modelArgsInputObjectTypes: DMMF.InputType[] = []; - const prismaVersion = getPrismaVersion(); - for (const model of models) { - const { name: modelName } = model; - const fields: DMMF.SchemaArg[] = []; - - const selectField: DMMF.SchemaArg = { - name: 'select', - isRequired: false, - isNullable: false, - inputTypes: [ - { - isList: false, - type: `${modelName}Select`, - location: 'inputObjectTypes', - namespace: 'prisma', - }, - ], - }; - fields.push(selectField); - - const hasRelationToAnotherModel = checkModelHasModelRelation(model); - - if (hasRelationToAnotherModel) { - const includeField: DMMF.SchemaArg = { - name: 'include', - isRequired: false, - isNullable: false, - inputTypes: [ - { - isList: false, - type: `${modelName}Include`, - location: 'inputObjectTypes', - namespace: 'prisma', - }, - ], - }; - fields.push(includeField); - } - - const modelArgsInputObjectType: DMMF.InputType = { - name: - prismaVersion && semver.gte(prismaVersion, '6.0.0') - ? `${modelName}DefaultArgs` // Prisma 6+ removed [Model]Args type - : `${modelName}Args`, - constraints: { - maxNumFields: null, - minNumFields: null, - }, - fields, - }; - modelArgsInputObjectTypes.push(modelArgsInputObjectType); - } - return modelArgsInputObjectTypes; -} diff --git a/packages/sdk/src/dmmf-helpers/select-helpers.ts b/packages/sdk/src/dmmf-helpers/select-helpers.ts deleted file mode 100644 index d22b601df..000000000 --- a/packages/sdk/src/dmmf-helpers/select-helpers.ts +++ /dev/null @@ -1,167 +0,0 @@ -import semver from 'semver'; -import { getPrismaVersion, type DMMF } from '../prisma'; -import { checkIsModelRelationField, checkModelHasManyModelRelation } from './model-helpers'; - -export function addMissingInputObjectTypesForSelect( - inputObjectTypes: DMMF.InputType[], - outputObjectTypes: DMMF.OutputType[], - models: readonly DMMF.Model[] -) { - // generate input object types necessary to support ModelSelect._count - const modelCountOutputTypes = getModelCountOutputTypes(outputObjectTypes); - const modelCountOutputTypeSelectInputObjectTypes = - generateModelCountOutputTypeSelectInputObjectTypes(modelCountOutputTypes); - const modelCountOutputTypeArgsInputObjectTypes = - generateModelCountOutputTypeArgsInputObjectTypes(modelCountOutputTypes); - - const modelSelectInputObjectTypes = generateModelSelectInputObjectTypes(models); - - const generatedInputObjectTypes = [ - modelCountOutputTypeSelectInputObjectTypes, - modelCountOutputTypeArgsInputObjectTypes, - modelSelectInputObjectTypes, - ].flat(); - - for (const inputObjectType of generatedInputObjectTypes) { - inputObjectTypes.push(inputObjectType); - } -} - -function getModelCountOutputTypes(outputObjectTypes: DMMF.OutputType[]) { - return outputObjectTypes.filter(({ name }) => name.includes('CountOutputType')); -} - -function generateModelCountOutputTypeSelectInputObjectTypes(modelCountOutputTypes: DMMF.OutputType[]) { - const modelCountOutputTypeSelectInputObjectTypes: DMMF.InputType[] = []; - for (const modelCountOutputType of modelCountOutputTypes) { - const { name: modelCountOutputTypeName, fields: modelCountOutputTypeFields } = modelCountOutputType; - const modelCountOutputTypeSelectInputObjectType: DMMF.InputType = { - name: `${modelCountOutputTypeName}Select`, - constraints: { - maxNumFields: null, - minNumFields: null, - }, - fields: modelCountOutputTypeFields.map(({ name }) => ({ - name, - isRequired: false, - isNullable: false, - inputTypes: [ - { - isList: false, - type: `Boolean`, - location: 'scalar', - }, - ], - })), - }; - modelCountOutputTypeSelectInputObjectTypes.push(modelCountOutputTypeSelectInputObjectType); - } - return modelCountOutputTypeSelectInputObjectTypes; -} - -function generateModelCountOutputTypeArgsInputObjectTypes(modelCountOutputTypes: DMMF.OutputType[]) { - const modelCountOutputTypeArgsInputObjectTypes: DMMF.InputType[] = []; - const prismaVersion = getPrismaVersion(); - for (const modelCountOutputType of modelCountOutputTypes) { - const { name: modelCountOutputTypeName } = modelCountOutputType; - const modelCountOutputTypeArgsInputObjectType: DMMF.InputType = { - name: - prismaVersion && semver.gte(prismaVersion, '6.0.0') - ? `${modelCountOutputTypeName}DefaultArgs` // Prisma 6+ removed [Model]Args type - : `${modelCountOutputTypeName}Args`, - constraints: { - maxNumFields: null, - minNumFields: null, - }, - fields: [ - { - name: 'select', - isRequired: false, - isNullable: false, - inputTypes: [ - { - isList: false, - type: `${modelCountOutputTypeName}Select`, - location: 'inputObjectTypes', - namespace: 'prisma', - }, - ], - }, - ], - }; - modelCountOutputTypeArgsInputObjectTypes.push(modelCountOutputTypeArgsInputObjectType); - } - return modelCountOutputTypeArgsInputObjectTypes; -} - -function generateModelSelectInputObjectTypes(models: readonly DMMF.Model[]) { - const modelSelectInputObjectTypes: DMMF.InputType[] = []; - const prismaVersion = getPrismaVersion(); - for (const model of models) { - const { name: modelName, fields: modelFields } = model; - const fields: DMMF.SchemaArg[] = []; - - for (const modelField of modelFields) { - const { name: modelFieldName, isList, type } = modelField; - const inputTypes = [{ isList: false, type: 'Boolean', location: 'scalar' }]; - - const isRelationField = checkIsModelRelationField(modelField); - if (isRelationField) { - const schemaArgInputType: DMMF.InputTypeRef = { - isList: false, - type: isList - ? `${type}FindManyArgs` - : prismaVersion && semver.gte(prismaVersion, '6.0.0') - ? `${type}DefaultArgs` // Prisma 6+ removed [Model]Args type - : `${type}Args`, - location: 'inputObjectTypes', - namespace: 'prisma', - }; - inputTypes.push(schemaArgInputType); - } - - const field: DMMF.SchemaArg = { - name: modelFieldName, - isRequired: false, - isNullable: false, - inputTypes: inputTypes as DMMF.InputTypeRef[], - }; - fields.push(field); - } - - const hasManyRelationToAnotherModel = checkModelHasManyModelRelation(model); - - const shouldAddCountField = hasManyRelationToAnotherModel; - if (shouldAddCountField) { - const _countField: DMMF.SchemaArg = { - name: '_count', - isRequired: false, - isNullable: false, - inputTypes: [ - { isList: false, type: 'Boolean', location: 'scalar' }, - { - isList: false, - type: - prismaVersion && semver.gte(prismaVersion, '6.0.0') - ? `${modelName}CountOutputTypeDefaultArgs` // Prisma 6+ removed [Model]CountOutputTypeArgs type - : `${modelName}CountOutputTypeArgs`, - location: 'inputObjectTypes', - namespace: 'prisma', - }, - ], - }; - fields.push(_countField); - } - - const modelSelectInputObjectType: DMMF.InputType = { - name: `${modelName}Select`, - constraints: { - maxNumFields: null, - minNumFields: null, - }, - fields, - }; - modelSelectInputObjectTypes.push(modelSelectInputObjectType); - } - return modelSelectInputObjectTypes; -} diff --git a/packages/sdk/src/dmmf-helpers/types.ts b/packages/sdk/src/dmmf-helpers/types.ts deleted file mode 100644 index a1fbc6c59..000000000 --- a/packages/sdk/src/dmmf-helpers/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { DMMF } from '../prisma'; - -export type TransformerParams = { - enumTypes?: DMMF.SchemaEnum[]; - fields?: DMMF.SchemaArg[]; - name?: string; - models?: DMMF.Model[]; - modelOperations?: DMMF.ModelMapping[]; - aggregateOperationSupport?: AggregateOperationSupport; - isDefaultPrismaClientOutput?: boolean; - prismaClientOutputPath?: string; -}; - -export type AggregateOperationSupport = { - [model: string]: { - count?: boolean; - min?: boolean; - max?: boolean; - sum?: boolean; - avg?: boolean; - }; -}; diff --git a/packages/sdk/src/model-meta-generator.ts b/packages/sdk/src/model-meta-generator.ts deleted file mode 100644 index 26e518782..000000000 --- a/packages/sdk/src/model-meta-generator.ts +++ /dev/null @@ -1,627 +0,0 @@ -import { - ArrayExpr, - DataModel, - DataModelAttribute, - DataModelField, - Expression, - isArrayExpr, - isBooleanLiteral, - isDataModel, - isDataModelField, - isInvocationExpr, - isNumberLiteral, - isObjectExpr, - isReferenceExpr, - isStringLiteral, - isTypeDef, - ObjectExpr, - ReferenceExpr, - TypeDef, - TypeDefField, -} from '@zenstackhq/language/ast'; -import type { RuntimeAttribute } from '@zenstackhq/runtime'; -import { lowerCaseFirst } from '@zenstackhq/runtime/local-helpers'; -import { streamAst } from 'langium'; -import { FunctionDeclarationStructure, OptionalKind, Project, VariableDeclarationKind } from 'ts-morph'; -import { match } from 'ts-pattern'; -import { - CodeWriter, - ExpressionContext, - FastWriter, - getAttribute, - getAttributeArg, - getAttributeArgs, - getAuthDecl, - getDataModels, - getInheritedFromDelegate, - getLiteral, - getRelationBackLink, - getRelationField, - hasAttribute, - isAuthInvocation, - isEnumFieldReference, - isForeignKeyField, - isIdField, - resolved, - saveSourceFile, - TypeScriptExpressionTransformer, -} from '.'; - -/** - * Options for generating model metadata - */ -export type ModelMetaGeneratorOptions = { - /** - * Output directory - */ - output: string; - - /** - * Whether to generate all attributes - */ - generateAttributes: boolean; - - /** - * Whether to preserve the pre-compilation TypeScript files - */ - preserveTsFiles?: boolean; - - /** - * Map from full names to shortened names, used for extra fields/relations generated by ZenStack - */ - shortNameMap?: Map; -}; - -export function generate( - project: Project, - models: DataModel[], - typeDefs: TypeDef[], - options: ModelMetaGeneratorOptions -) { - const sf = project.createSourceFile(options.output, undefined, { overwrite: true }); - - // generate: import type { ModelMeta } from '@zenstackhq/runtime'; - sf.addImportDeclaration({ - isTypeOnly: true, - namedImports: ['ModelMeta'], - moduleSpecifier: '@zenstackhq/runtime', - }); - - const writer = new FastWriter(); - const extraFunctions: OptionalKind[] = []; - generateModelMetadata(models, typeDefs, writer, options, extraFunctions); - - sf.addVariableStatement({ - declarationKind: VariableDeclarationKind.Const, - declarations: [{ name: 'metadata', type: 'ModelMeta', initializer: writer.result }], - }); - - if (extraFunctions.length > 0) { - sf.addFunctions(extraFunctions); - } - - sf.addStatements('export default metadata;'); - - if (options.preserveTsFiles) { - saveSourceFile(sf); - } - - return sf; -} - -function generateModelMetadata( - dataModels: DataModel[], - typeDefs: TypeDef[], - writer: CodeWriter, - options: ModelMetaGeneratorOptions, - extraFunctions: OptionalKind[] -) { - writer.block(() => { - writeModels(writer, dataModels, options, extraFunctions); - writeTypeDefs(writer, typeDefs, options, extraFunctions); - writeDeleteCascade(writer, dataModels); - writeShortNameMap(options, writer); - writeAuthModel(writer, dataModels, typeDefs); - }); -} - -function writeModels( - writer: CodeWriter, - dataModels: DataModel[], - options: ModelMetaGeneratorOptions, - extraFunctions: OptionalKind[] -) { - writer.write('models:'); - writer.block(() => { - for (const model of dataModels) { - writer.write(`${lowerCaseFirst(model.name)}:`); - writer.block(() => { - writer.write(`name: '${model.name}',`); - writeBaseTypes(writer, model); - writeFields(writer, model, options, extraFunctions); - writeUniqueConstraints(writer, model); - if (options.generateAttributes) { - writeModelAttributes(writer, model); - } - writeDiscriminator(writer, model); - }); - writer.writeLine(','); - } - }); - writer.writeLine(','); -} - -function writeTypeDefs( - writer: CodeWriter, - typedDefs: TypeDef[], - options: ModelMetaGeneratorOptions, - extraFunctions: OptionalKind[] -) { - if (typedDefs.length === 0) { - return; - } - writer.write('typeDefs:'); - writer.block(() => { - for (const typeDef of typedDefs) { - writer.write(`${lowerCaseFirst(typeDef.name)}:`); - writer.block(() => { - writer.write(`name: '${typeDef.name}',`); - writeFields(writer, typeDef, options, extraFunctions); - }); - writer.writeLine(','); - } - }); - writer.writeLine(','); -} - -function writeBaseTypes(writer: CodeWriter, model: DataModel) { - if (model.superTypes.length > 0) { - writer.write('baseTypes: ['); - writer.write(model.superTypes.map((t) => `'${t.ref?.name}'`).join(', ')); - writer.write('],'); - } -} - -function writeAuthModel(writer: CodeWriter, dataModels: DataModel[], typeDefs: TypeDef[]) { - const authModel = getAuthDecl([...dataModels, ...typeDefs]); - if (authModel) { - writer.writeLine(`authModel: '${authModel.name}'`); - } -} - -function writeDeleteCascade(writer: CodeWriter, dataModels: DataModel[]) { - writer.write('deleteCascade:'); - writer.block(() => { - for (const model of dataModels) { - const cascades = getDeleteCascades(model); - if (cascades.length > 0) { - writer.writeLine(`${lowerCaseFirst(model.name)}: [${cascades.map((n) => `'${n}'`).join(', ')}],`); - } - } - }); - writer.writeLine(','); -} - -function writeUniqueConstraints(writer: CodeWriter, model: DataModel) { - const constraints = getUniqueConstraints(model); - if (constraints.length > 0) { - writer.write('uniqueConstraints:'); - writer.block(() => { - for (const constraint of constraints) { - writer.write(`${constraint.name}: { - name: "${constraint.name}", - fields: ${JSON.stringify(constraint.fields)} - },`); - } - }); - writer.write(','); - } -} - -function writeModelAttributes(writer: CodeWriter, model: DataModel) { - const attrs = getAttributes(model); - if (attrs.length > 0) { - writer.write(` -attributes: ${JSON.stringify(attrs)},`); - } -} - -function writeDiscriminator(writer: CodeWriter, model: DataModel) { - const delegateAttr = getAttribute(model, '@@delegate'); - if (!delegateAttr) { - return; - } - const discriminator = getAttributeArg(delegateAttr, 'discriminator') as ReferenceExpr; - if (!discriminator) { - return; - } - if (discriminator) { - writer.write(`discriminator: ${JSON.stringify(discriminator.target.$refText)},`); - } -} - -function writeFields( - writer: CodeWriter, - container: DataModel | TypeDef, - options: ModelMetaGeneratorOptions, - extraFunctions: OptionalKind[] -) { - writer.write('fields:'); - writer.block(() => { - for (const f of container.fields) { - const dmField = isDataModelField(f) ? f : undefined; - - writer.write(`${f.name}: {`); - - writer.write(` - name: "${f.name}", - type: "${ - f.type.reference - ? f.type.reference.$refText - : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - f.type.type! - }",`); - - if (dmField && isIdField(dmField)) { - writer.write(` - isId: true,`); - } - - if (isDataModel(f.type.reference?.ref)) { - writer.write(` - isDataModel: true,`); - } else if (isTypeDef(f.type.reference?.ref)) { - writer.write(` - isTypeDef: true,`); - } - - if (f.type.array) { - writer.write(` - isArray: true,`); - } - - if (f.type.optional) { - writer.write(` - isOptional: true,`); - } - - if (options.generateAttributes) { - const attrs = getAttributes(f); - if (attrs.length > 0) { - writer.write(` - attributes: ${JSON.stringify(attrs)},`); - } - } else { - // only include essential attributes - const attrs = getAttributes(f).filter((attr) => ['@default', '@updatedAt'].includes(attr.name)); - if (attrs.length > 0) { - writer.write(` - attributes: ${JSON.stringify(attrs)},`); - } - } - - const defaultValueProvider = generateDefaultValueProvider(f, extraFunctions); - if (defaultValueProvider) { - writer.write(` - defaultValueProvider: ${defaultValueProvider},`); - } - - if (dmField) { - // metadata specific to DataModelField - - const backlink = getRelationBackLink(dmField); - const fkMapping = generateForeignKeyMapping(dmField); - - if (backlink) { - writer.write(` - backLink: '${backlink.name}',`); - } - - if (isRelationOwner(dmField, backlink)) { - writer.write(` - isRelationOwner: true,`); - } - - const onDeleteAction = getOnDeleteAction(dmField); - if (onDeleteAction) { - writer.write(` - onDeleteAction: '${onDeleteAction}',`); - } - - const onUpdateAction = getOnUpdateAction(dmField); - if (onUpdateAction) { - writer.write(` - onUpdateAction: '${onUpdateAction}',`); - } - - if (isForeignKeyField(dmField)) { - writer.write(` - isForeignKey: true,`); - const relationField = getRelationField(dmField); - if (relationField) { - writer.write(` - relationField: '${relationField.name}',`); - } - } - - if (fkMapping && Object.keys(fkMapping).length > 0) { - writer.write(` - foreignKeyMapping: ${JSON.stringify(fkMapping)},`); - } - - const inheritedFromDelegate = getInheritedFromDelegate(dmField); - if (inheritedFromDelegate && !isIdField(dmField)) { - writer.write(` - inheritedFrom: ${JSON.stringify(inheritedFromDelegate.name)},`); - } - - if (isAutoIncrement(dmField)) { - writer.write(` - isAutoIncrement: true,`); - } - } - - writer.write(` - },`); - } - }); - writer.write(','); -} - -function getAttributes(target: DataModelField | DataModel | TypeDefField): RuntimeAttribute[] { - return target.attributes - .map((attr) => { - const args: Array<{ name?: string; value?: unknown }> = []; - for (const arg of attr.args) { - const argName = arg.$resolvedParam?.name ?? arg.name; - const argValue = exprToValue(arg.value); - args.push({ name: argName, value: argValue }); - } - return { name: resolved(attr.decl).name, args }; - }) - .filter((d): d is RuntimeAttribute => !!d); -} - -function getUniqueConstraints(model: DataModel) { - const constraints: Array<{ name: string; fields: string[] }> = []; - - const extractConstraint = (attr: DataModelAttribute) => { - const argsMap = getAttributeArgs(attr); - if (argsMap.fields) { - const fieldNames = (argsMap.fields as ArrayExpr).items.map( - (item) => resolved((item as ReferenceExpr).target).name - ); - let constraintName = argsMap.name && getLiteral(argsMap.name); - if (!constraintName) { - // default constraint name is fields concatenated with underscores - constraintName = fieldNames.join('_'); - } - return { name: constraintName, fields: fieldNames }; - } else { - return undefined; - } - }; - - const addConstraint = (constraint: { name: string; fields: string[] }) => { - if (!constraints.some((c) => c.name === constraint.name)) { - constraints.push(constraint); - } - }; - - // field-level @id first - for (const field of model.fields) { - if (hasAttribute(field, '@id')) { - addConstraint({ name: field.name, fields: [field.name] }); - } - } - - // then model-level @@id - for (const attr of model.attributes.filter((attr) => attr.decl.ref?.name === '@@id')) { - const constraint = extractConstraint(attr); - if (constraint) { - addConstraint(constraint); - } - } - - // then field-level @unique - for (const field of model.fields) { - if (hasAttribute(field, '@unique')) { - addConstraint({ name: field.name, fields: [field.name] }); - } - } - - // then model-level @@unique - for (const attr of model.attributes.filter((attr) => attr.decl.ref?.name === '@@unique')) { - const constraint = extractConstraint(attr); - if (constraint) { - addConstraint(constraint); - } - } - - return constraints; -} - -function isRelationOwner(field: DataModelField, backLink: DataModelField | undefined) { - if (!isDataModel(field.type.reference?.ref)) { - return false; - } - - if (!backLink) { - // CHECKME: can this really happen? - return true; - } - - if (!hasAttribute(field, '@relation') && !hasAttribute(backLink, '@relation')) { - // if neither side has `@relation` attribute, it's an implicit many-to-many relation, - // both sides are owners - return true; - } - - return holdsForeignKey(field); -} - -function holdsForeignKey(field: DataModelField) { - const relation = field.attributes.find((attr) => attr.decl.ref?.name === '@relation'); - if (!relation) { - return false; - } - const fields = getAttributeArg(relation, 'fields'); - return !!fields; -} - -function generateForeignKeyMapping(field: DataModelField) { - const relation = field.attributes.find((attr) => attr.decl.ref?.name === '@relation'); - if (!relation) { - return undefined; - } - const fields = getAttributeArg(relation, 'fields'); - const references = getAttributeArg(relation, 'references'); - if (!isArrayExpr(fields) || !isArrayExpr(references) || fields.items.length !== references.items.length) { - return undefined; - } - - const fieldNames = fields.items.map((item) => (isReferenceExpr(item) ? item.target.$refText : undefined)); - const referenceNames = references.items.map((item) => (isReferenceExpr(item) ? item.target.$refText : undefined)); - - const result: Record = {}; - referenceNames.forEach((name, i) => { - if (name) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - result[name] = fieldNames[i]!; - } - }); - return result; -} - -function getDeleteCascades(model: DataModel): string[] { - const allModels = getDataModels(model.$container); - return allModels - .filter((m) => { - if (m === model) { - return false; - } - const relationFields = m.fields.filter((f) => { - if (f.type.reference?.ref !== model) { - return false; - } - const relationAttr = getAttribute(f, '@relation'); - if (relationAttr) { - const onDelete = getAttributeArg(relationAttr, 'onDelete'); - if (onDelete && isEnumFieldReference(onDelete) && onDelete.target.ref?.name === 'Cascade') { - return true; - } - } - return false; - }); - return relationFields.length > 0; - }) - .map((m) => m.name); -} - -function generateDefaultValueProvider( - field: DataModelField | TypeDefField, - extraFunctions: OptionalKind[] -) { - const defaultAttr = getAttribute(field, '@default'); - if (!defaultAttr) { - return undefined; - } - - const expr = defaultAttr.args[0]?.value; - if (!expr) { - return undefined; - } - - // find `auth()` in default value expression - const hasAuth = streamAst(expr).some(isAuthInvocation); - if (!hasAuth) { - return undefined; - } - - // generates a provider function like: - // function $default$Model$field(user: any) { ... } - const funcName = `$default$${field.$container.name}$${field.name}`; - extraFunctions.push({ - name: funcName, - parameters: [{ name: 'user', type: 'any' }], - returnType: 'unknown', - statements: (writer) => { - const tsWriter = new TypeScriptExpressionTransformer({ context: ExpressionContext.DefaultValue }); - const code = tsWriter.transform(expr, false); - writer.write(`return ${code};`); - }, - }); - - return funcName; -} - -function isAutoIncrement(field: DataModelField) { - const defaultAttr = getAttribute(field, '@default'); - if (!defaultAttr) { - return false; - } - - const arg = defaultAttr.args[0]?.value; - if (!arg) { - return false; - } - - return isInvocationExpr(arg) && arg.function.$refText === 'autoincrement'; -} - -function writeShortNameMap(options: ModelMetaGeneratorOptions, writer: CodeWriter) { - if (options.shortNameMap && options.shortNameMap.size > 0) { - writer.write('shortNameMap:'); - writer.block(() => { - for (const [key, value] of options.shortNameMap!) { - writer.write(`${key}: '${value}',`); - } - }); - writer.write(','); - } -} - -function getOnDeleteAction(fieldInfo: DataModelField) { - const relationAttr = getAttribute(fieldInfo, '@relation'); - if (relationAttr) { - const onDelete = getAttributeArg(relationAttr, 'onDelete'); - if (onDelete && isEnumFieldReference(onDelete)) { - return onDelete.target.ref?.name; - } - } - return undefined; -} - -function getOnUpdateAction(fieldInfo: DataModelField) { - const relationAttr = getAttribute(fieldInfo, '@relation'); - if (relationAttr) { - const onUpdate = getAttributeArg(relationAttr, 'onUpdate'); - if (onUpdate && isEnumFieldReference(onUpdate)) { - return onUpdate.target.ref?.name; - } - } - return undefined; -} - -function exprToValue(value: Expression): unknown { - return match(value) - .when(isStringLiteral, (v) => v.value) - .when(isBooleanLiteral, (v) => v.value) - .when(isNumberLiteral, (v) => { - let num = parseInt(v.value); - if (isNaN(num)) { - num = parseFloat(v.value); - } - if (isNaN(num)) { - return undefined; - } - return num; - }) - .when(isArrayExpr, (v) => v.items.map((item) => exprToValue(item))) - .when(isObjectExpr, (v) => exprToObject(v)) - .otherwise(() => undefined); -} - -function exprToObject(value: ObjectExpr): unknown { - return Object.fromEntries(value.fields.map((field) => [field.name, exprToValue(field.value)])); -} diff --git a/packages/sdk/src/names.ts b/packages/sdk/src/names.ts deleted file mode 100644 index be78396a3..000000000 --- a/packages/sdk/src/names.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DataModel, DataModelField } from './ast'; - -/** - * Gets the name of the function that computes a partial Prisma query guard. - */ -export function getQueryGuardFunctionName( - model: DataModel, - forField: DataModelField | undefined, - fieldOverride: boolean, - kind: string -) { - return `${model.name}${forField ? '$' + forField.name : ''}${fieldOverride ? '$override' : ''}_${kind}`; -} - -/** - * Gets the name of the function that checks an entity for access policy rules. - */ -export function getEntityCheckerFunctionName( - model: DataModel, - forField: DataModelField | undefined, - fieldOverride: boolean, - kind: string -) { - return `$check_${model.name}${forField ? '$' + forField.name : ''}${fieldOverride ? '$override' : ''}_${kind}`; -} diff --git a/packages/sdk/src/package.json b/packages/sdk/src/package.json deleted file mode 120000 index 4e26811d4..000000000 --- a/packages/sdk/src/package.json +++ /dev/null @@ -1 +0,0 @@ -../package.json \ No newline at end of file diff --git a/packages/sdk/src/path.ts b/packages/sdk/src/path.ts deleted file mode 100644 index 618abb751..000000000 --- a/packages/sdk/src/path.ts +++ /dev/null @@ -1,9 +0,0 @@ -import path from 'path'; - -/** - * Gets the relative path from `from` to `to` and normalizes it to start it with `./` - */ -export function normalizedRelative(from: string, to: string) { - const result = path.relative(from, to); - return result.startsWith('.') ? result : `./${result}`; -} diff --git a/packages/sdk/src/policy.ts b/packages/sdk/src/policy.ts deleted file mode 100644 index c9eea9865..000000000 --- a/packages/sdk/src/policy.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { DataModel, DataModelAttribute } from './ast'; -import { getLiteral } from './utils'; -import { hasValidationAttributes } from './validation'; - -export type PolicyAnalysisResult = ReturnType; - -export function analyzePolicies(dataModel: DataModel) { - const allows = dataModel.attributes.filter((attr) => attr.decl.ref?.name === '@@allow'); - const denies = dataModel.attributes.filter((attr) => attr.decl.ref?.name === '@@deny'); - - const create = toStaticPolicy('create', allows, denies); - const read = toStaticPolicy('read', allows, denies); - const update = toStaticPolicy('update', allows, denies); - const del = toStaticPolicy('delete', allows, denies); - const hasFieldValidation = hasValidationAttributes(dataModel); - - return { - allows, - denies, - create, - read, - update, - delete: del, - allowAll: create === true && read === true && update === true && del === true, - denyAll: create === false && read === false && update === false && del === false, - hasFieldValidation, - }; -} - -function toStaticPolicy( - operation: string, - allows: DataModelAttribute[], - denies: DataModelAttribute[] -): boolean | undefined { - const filteredDenies = forOperation(operation, denies); - if (filteredDenies.some((rule) => getLiteral(rule.args[1].value) === true)) { - // any constant true deny rule - return false; - } - - const filteredAllows = forOperation(operation, allows); - if (filteredAllows.length === 0) { - // no allow rule - return false; - } - - if ( - filteredDenies.length === 0 && - filteredAllows.some((rule) => getLiteral(rule.args[1].value) === true) - ) { - // any constant true allow rule - return true; - } - return undefined; -} - -function forOperation(operation: string, rules: DataModelAttribute[]) { - return rules.filter((rule) => { - const ops = getLiteral(rule.args[0].value); - if (!ops) { - return false; - } - if (ops === 'all') { - return true; - } - const splitOps = ops.split(',').map((p) => p.trim()); - return splitOps.includes(operation); - }); -} diff --git a/packages/sdk/src/prisma.ts b/packages/sdk/src/prisma.ts deleted file mode 100644 index 5b7b4d6a1..000000000 --- a/packages/sdk/src/prisma.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -import type { DMMF } from '@prisma/generator-helper'; -import { getDMMF as _getDMMF, type GetDMMFOptions } from '@prisma/internals'; -import { getDMMF as _getDMMF7 } from '@prisma/internals-v7'; -import { DEFAULT_RUNTIME_LOAD_PATH } from '@zenstackhq/runtime'; -import path from 'path'; -import semver from 'semver'; -import { Model } from './ast'; -import { RUNTIME_PACKAGE } from './constants'; -import { normalizedRelative } from './path'; -import type { PluginOptions } from './types'; -import { getDataSourceProvider } from './utils'; - -/** - * Given an import context directory and plugin options, compute the import spec for the Prisma Client. - */ -export function getPrismaClientImportSpec(importingFromDir: string, options: PluginOptions) { - if (!options.prismaClientPath || options.prismaClientPath === '@prisma/client') { - return '@prisma/client'; - } - - if ( - options.prismaClientPath.startsWith(RUNTIME_PACKAGE) || - options.prismaClientPath.startsWith(DEFAULT_RUNTIME_LOAD_PATH) - ) { - return options.prismaClientPath; - } - - if (path.isAbsolute(options.prismaClientPath)) { - // absolute path - return options.prismaClientPath; - } - - // resolve absolute path based on the zmodel file location - const resolvedPrismaClientOutput = path.resolve(path.dirname(options.schemaPath), options.prismaClientPath); - - // translate to path relative to the importing context directory - let result = normalizedRelative(importingFromDir, resolvedPrismaClientOutput); - - // remove leading `node_modules` (which may be provided by the user) - result = result.replace(/^([./\\]*)?node_modules\//, ''); - - return normalizePath(result); -} - -function normalizePath(p: string) { - return p ? p.split(path.sep).join(path.posix.sep) : p; -} - -/** - * Loads Prisma DMMF - */ -export function getDMMF(options: GetDMMFOptions): Promise { - const prismaVersion = getPrismaVersion(); - if (prismaVersion && semver.gte(prismaVersion, '7.0.0')) { - return _getDMMF7(options); - } else { - return _getDMMF(options); - } -} - -/** - * Gets the installed Prisma's version - */ -export function getPrismaVersion(): string | undefined { - if (process.env.ZENSTACK_TEST === '1') { - // test environment - try { - return require(path.resolve('./node_modules/@prisma/client/package.json')).version; - } catch { - return undefined; - } - } - - try { - return require('@prisma/client/package.json').version; - } catch { - try { - return require('prisma/package.json').version; - } catch { - return undefined; - } - } -} - -/** - * Returns if the given model supports `createMany` operation. - */ -export function supportCreateMany(model: Model) { - // `createMany` is supported for sqlite since Prisma 5.12.0 - const prismaVersion = getPrismaVersion(); - const dsProvider = getDataSourceProvider(model); - return dsProvider !== 'sqlite' || (prismaVersion && semver.gte(prismaVersion, '5.12.0')); -} - -export type { DMMF } from '@prisma/generator-helper'; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts deleted file mode 100644 index 29160c283..000000000 --- a/packages/sdk/src/types.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { DMMF } from '@prisma/generator-helper'; -import { Model } from '@zenstackhq/language/ast'; -import type { Project } from 'ts-morph'; - -/** - * Plugin configuration option value type - */ -export type OptionValue = string | number | boolean; - -/** - * Plugin configuration options - */ -export type PluginDeclaredOptions = { - /*** - * The provider package - */ - provider: string; -} & Record; - -/** - * Plugin configuration options for execution - */ -export type PluginOptions = { - /** - * ZModel schema absolute path - */ - schemaPath: string; - - /** - * PrismaClient import path, either relative to `schemaPath` or absolute - */ - prismaClientPath?: string; - - /** - * PrismaClient's TypeScript declaration file's path - * @private - */ - prismaClientDtsPath?: string; - - /** - * An optional map of full names to shortened names - * @private - */ - shortNameMap?: Map; -} & PluginDeclaredOptions; - -/** - * Global options that apply to all plugins - */ -export type PluginGlobalOptions = { - /** - * Default output directory - */ - output?: string; - - /** - * Whether to compile the generated code - */ - compile: boolean; - - /** - * The `ts-morph` project used for code generation. - * @private - */ - tsProject: Project; -}; - -/** - * Plugin run results. - */ -export type PluginResult = { - /** - * Warnings - */ - warnings: string[]; - - /** - * PrismaClient path, either relative to zmodel path or absolute, if the plugin - * generated a PrismaClient - */ - prismaClientPath?: string; - - /** - * PrismaClient's TypeScript declaration file's path - * @private - */ - prismaClientDtsPath?: string; - - /** - * An optional Prisma DMMF document that a plugin can generate - * @private - */ - dmmf?: DMMF.Document; - - /** - * An optional map of full names to shortened names - * @private - */ - shortNameMap?: Map; -}; - -/** - * Plugin entry point function definition - */ -export type PluginFunction = ( - model: Model, - options: PluginOptions, - dmmf: DMMF.Document | undefined, - globalOptions?: PluginGlobalOptions -) => Promise | PluginResult | Promise | void; - -/** - * Plugin error - */ -export class PluginError extends Error { - constructor(public plugin: string, message: string) { - super(message); - } -} diff --git a/packages/sdk/src/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts deleted file mode 100644 index c653c52fa..000000000 --- a/packages/sdk/src/typescript-expression-transformer.ts +++ /dev/null @@ -1,573 +0,0 @@ -import { - ArrayExpr, - BinaryExpr, - BooleanLiteral, - DataModel, - Expression, - InvocationExpr, - LiteralExpr, - MemberAccessExpr, - NullExpr, - NumberLiteral, - ReferenceExpr, - StringLiteral, - ThisExpr, - UnaryExpr, - isArrayExpr, - isDataModel, - isEnumField, - isInvocationExpr, - isLiteralExpr, - isNullExpr, - isThisExpr, -} from '@zenstackhq/language/ast'; -import { getContainerOfType } from 'langium'; -import { P, match } from 'ts-pattern'; -import { ExpressionContext } from './constants'; -import { getEntityCheckerFunctionName } from './names'; -import { getIdFields, getLiteral, isDataModelFieldReference, isFromStdlib, isFutureExpr } from './utils'; - -export class TypeScriptExpressionTransformerError extends Error { - constructor(message: string) { - super(message); - } -} - -type Options = { - isPostGuard?: boolean; - fieldReferenceContext?: string; - thisExprContext?: string; - futureRefContext?: string; - context: ExpressionContext; - operationContext?: 'read' | 'create' | 'update' | 'postUpdate' | 'delete'; - useLiteralEnum?: boolean; -}; - -type Casing = 'original' | 'upper' | 'lower' | 'capitalize' | 'uncapitalize'; - -// a registry of function handlers marked with @func -const functionHandlers = new Map(); - -// function handler decorator -function func(name: string) { - return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { - if (!functionHandlers.get(name)) { - functionHandlers.set(name, descriptor); - } - return descriptor; - }; -} - -/** - * Transforms ZModel expression to plain TypeScript expression. - */ -export class TypeScriptExpressionTransformer { - /** - * Constructs a new TypeScriptExpressionTransformer. - * - * @param isPostGuard indicates if we're writing for post-update conditions - */ - constructor(private readonly options: Options) {} - - /** - * Transforms the given expression to a TypeScript expression. - * @param normalizeUndefined if undefined values should be normalized to null - * @returns - */ - transform(expr: Expression, normalizeUndefined = true): string { - switch (expr.$type) { - case StringLiteral: - case NumberLiteral: - case BooleanLiteral: - return this.literal(expr as LiteralExpr); - - case ArrayExpr: - return this.array(expr as ArrayExpr, normalizeUndefined); - - case NullExpr: - return this.null(); - - case ThisExpr: - return this.this(expr as ThisExpr); - - case ReferenceExpr: - return this.reference(expr as ReferenceExpr); - - case InvocationExpr: - return this.invocation(expr as InvocationExpr, normalizeUndefined); - - case MemberAccessExpr: - return this.memberAccess(expr as MemberAccessExpr, normalizeUndefined); - - case UnaryExpr: - return this.unary(expr as UnaryExpr, normalizeUndefined); - - case BinaryExpr: - return this.binary(expr as BinaryExpr, normalizeUndefined); - - default: - throw new TypeScriptExpressionTransformerError(`Unsupported expression type: ${expr.$type}`); - } - } - - private this(_expr: ThisExpr) { - // "this" is mapped to the input argument - return this.options.thisExprContext ?? 'input'; - } - - private memberAccess(expr: MemberAccessExpr, normalizeUndefined: boolean) { - if (!expr.member.ref) { - throw new TypeScriptExpressionTransformerError(`Unresolved MemberAccessExpr`); - } - - if (isFutureExpr(expr.operand)) { - if (this.options?.isPostGuard !== true) { - throw new TypeScriptExpressionTransformerError(`future() is only supported in postUpdate rules`); - } - return this.options.futureRefContext - ? `${this.options.futureRefContext}.${expr.member.ref.name}` - : expr.member.ref.name; - } else { - if (normalizeUndefined) { - // normalize field access to null instead of undefined to avoid accidentally use undefined in filter - return `(${this.transform(expr.operand, normalizeUndefined)}?.${expr.member.ref.name} ?? null)`; - } else { - return `${this.transform(expr.operand, normalizeUndefined)}?.${expr.member.ref.name}`; - } - } - } - - private invocation(expr: InvocationExpr, normalizeUndefined: boolean) { - if (!expr.function.ref) { - throw new TypeScriptExpressionTransformerError(`Unresolved InvocationExpr`); - } - - const funcName = expr.function.ref.name; - const isStdFunc = isFromStdlib(expr.function.ref); - - if (!isStdFunc) { - throw new TypeScriptExpressionTransformerError('User-defined functions are not supported yet'); - } - - const handler = functionHandlers.get(funcName); - if (!handler) { - throw new TypeScriptExpressionTransformerError(`Unsupported function: ${funcName}`); - } - - const args = expr.args.map((arg) => arg.value); - return handler.value.call(this, expr, args, normalizeUndefined); - } - - // #region function invocation handlers - - // arguments have been type-checked - - @func('auth') - private _auth() { - return 'user'; - } - - @func('now') - private _now() { - return `(new Date())`; - } - - @func('length') - private _length(_invocation: InvocationExpr, args: Expression[]) { - const field = this.transform(args[0], false); - const min = getLiteral(args[1]); - const max = getLiteral(args[2]); - let result: string; - if (min === undefined) { - result = this.ensureBooleanTernary(args[0], field, `${field}?.length > 0`); - } else if (max === undefined) { - result = this.ensureBooleanTernary(args[0], field, `${field}?.length >= ${min}`); - } else { - result = this.ensureBooleanTernary( - args[0], - field, - `${field}?.length >= ${min} && ${field}?.length <= ${max}` - ); - } - return result; - } - - @func('contains') - private _contains(_invocation: InvocationExpr, args: Expression[], normalizeUndefined: boolean) { - const field = this.transform(args[0], false); - const caseInsensitive = getLiteral(args[2]) === true; - let result: string; - if (caseInsensitive) { - result = `${field}?.toLowerCase().includes(${this.transform(args[1], normalizeUndefined)}?.toLowerCase())`; - } else { - result = `${field}?.includes(${this.transform(args[1], normalizeUndefined)})`; - } - return this.ensureBoolean(result); - } - - @func('startsWith') - private _startsWith(_invocation: InvocationExpr, args: Expression[], normalizeUndefined: boolean) { - const field = this.transform(args[0], false); - const result = `${field}?.startsWith(${this.transform(args[1], normalizeUndefined)})`; - return this.ensureBoolean(result); - } - - @func('endsWith') - private _endsWith(_invocation: InvocationExpr, args: Expression[], normalizeUndefined: boolean) { - const field = this.transform(args[0], false); - const result = `${field}?.endsWith(${this.transform(args[1], normalizeUndefined)})`; - return this.ensureBoolean(result); - } - - @func('regex') - private _regex(_invocation: InvocationExpr, args: Expression[]) { - const field = this.transform(args[0], false); - const pattern = getLiteral(args[1]); - return this.ensureBooleanTernary(args[0], field, `new RegExp(${JSON.stringify(pattern)}).test(${field})`); - } - - @func('email') - private _email(_invocation: InvocationExpr, args: Expression[]) { - const field = this.transform(args[0], false); - return this.ensureBooleanTernary(args[0], field, `z.string().email().safeParse(${field}).success`); - } - - @func('datetime') - private _datetime(_invocation: InvocationExpr, args: Expression[]) { - const field = this.transform(args[0], false); - return this.ensureBooleanTernary( - args[0], - field, - `z.string().datetime({ offset: true }).safeParse(${field}).success` - ); - } - - @func('url') - private _url(_invocation: InvocationExpr, args: Expression[]) { - const field = this.transform(args[0], false); - return this.ensureBooleanTernary(args[0], field, `z.string().url().safeParse(${field}).success`); - } - - @func('has') - private _has(_invocation: InvocationExpr, args: Expression[], normalizeUndefined: boolean) { - const field = this.transform(args[0], false); - const result = `${field}?.includes(${this.transform(args[1], normalizeUndefined)})`; - return this.ensureBoolean(result); - } - - @func('hasEvery') - private _hasEvery(_invocation: InvocationExpr, args: Expression[], normalizeUndefined: boolean) { - const field = this.transform(args[0], false); - return this.ensureBooleanTernary( - args[0], - field, - `${this.transform(args[1], normalizeUndefined)}?.every((item) => ${field}?.includes(item))` - ); - } - - @func('hasSome') - private _hasSome(_invocation: InvocationExpr, args: Expression[], normalizeUndefined: boolean) { - const field = this.transform(args[0], false); - return this.ensureBooleanTernary( - args[0], - field, - `${this.transform(args[1], normalizeUndefined)}?.some((item) => ${field}?.includes(item))` - ); - } - - @func('isEmpty') - private _isEmpty(_invocation: InvocationExpr, args: Expression[]) { - const field = this.transform(args[0], false); - return `(!${field} || ${field}?.length === 0)`; - } - - @func('check') - private _check(_invocation: InvocationExpr, args: Expression[]) { - if (!isDataModelFieldReference(args[0])) { - throw new TypeScriptExpressionTransformerError(`First argument of check() must be a field`); - } - if (!isDataModel(args[0].$resolvedType?.decl)) { - throw new TypeScriptExpressionTransformerError(`First argument of check() must be a relation field`); - } - - const fieldRef = args[0] as ReferenceExpr; - const targetModel = fieldRef.$resolvedType?.decl as DataModel; - - let operation: string; - if (args[1]) { - const literal = getLiteral(args[1]); - if (!literal) { - throw new TypeScriptExpressionTransformerError(`Second argument of check() must be a string literal`); - } - if (!['read', 'create', 'update', 'delete'].includes(literal)) { - throw new TypeScriptExpressionTransformerError(`Invalid check() operation "${literal}"`); - } - operation = literal; - } else { - if (!this.options.operationContext) { - throw new TypeScriptExpressionTransformerError('Unable to determine CRUD operation from context'); - } - operation = this.options.operationContext; - } - - const entityCheckerFunc = getEntityCheckerFunctionName(targetModel, undefined, false, operation); - return `${entityCheckerFunc}(input.${fieldRef.target.$refText}, context)`; - } - - private toStringWithCaseChange(value: string, casing: Casing) { - if (!value) { - return "''"; - } - return match(casing) - .with('original', () => `'${value}'`) - .with('upper', () => `'${value.toUpperCase()}'`) - .with('lower', () => `'${value.toLowerCase()}'`) - .with('capitalize', () => `'${value.charAt(0).toUpperCase() + value.slice(1)}'`) - .with('uncapitalize', () => `'${value.charAt(0).toLowerCase() + value.slice(1)}'`) - .exhaustive(); - } - - @func('currentModel') - private _currentModel(invocation: InvocationExpr, args: Expression[]) { - let casing: Casing = 'original'; - if (args[0]) { - casing = getLiteral(args[0]) as Casing; - } - - const containingModel = getContainerOfType(invocation, isDataModel); - if (!containingModel) { - throw new TypeScriptExpressionTransformerError('currentModel() must be called inside a model'); - } - return this.toStringWithCaseChange(containingModel.name, casing); - } - - @func('currentOperation') - private _currentOperation(_invocation: InvocationExpr, args: Expression[]) { - let casing: Casing = 'original'; - if (args[0]) { - casing = getLiteral(args[0]) as Casing; - } - - if (!this.options.operationContext) { - throw new TypeScriptExpressionTransformerError( - 'currentOperation() must be called inside an access policy rule' - ); - } - let contextOperation = this.options.operationContext; - if (contextOperation === 'postUpdate') { - contextOperation = 'update'; - } - return this.toStringWithCaseChange(contextOperation, casing); - } - - private ensureBoolean(expr: string) { - if (this.options.context === ExpressionContext.ValidationRule) { - // all fields are optional in a validation context, so we treat undefined - // as boolean true - return `(${expr} ?? true)`; - } else { - return `((${expr}) ?? false)`; - } - } - - private ensureBooleanTernary(predicate: Expression, transformedPredicate: string, value: string) { - if (isLiteralExpr(predicate) || isArrayExpr(predicate)) { - // these are never undefined - return value; - } - - if (this.options.context === ExpressionContext.ValidationRule) { - // all fields are optional in a validation context, so we treat undefined - // as boolean true - return `((${transformedPredicate}) !== undefined ? (${value}): true)`; - } else { - return `((${transformedPredicate}) !== undefined ? (${value}): false)`; - } - } - - // #endregion - - private reference(expr: ReferenceExpr) { - if (!expr.target.ref) { - throw new TypeScriptExpressionTransformerError(`Unresolved ReferenceExpr`); - } - - if (isEnumField(expr.target.ref)) { - return this.options.useLiteralEnum - ? JSON.stringify(expr.target.ref.name) - : `${expr.target.ref.$container.name}.${expr.target.ref.name}`; - } else { - if (this.options?.isPostGuard) { - // if we're processing post-update, any direct field access should be - // treated as access to context.preValue, which is entity's value before - // the update - return `context.preValue?.${expr.target.ref.name}`; - } else { - return this.options?.fieldReferenceContext - ? `${this.options.fieldReferenceContext}?.${expr.target.ref.name}` - : expr.target.ref.name; - } - } - } - - private null() { - return 'null'; - } - - private array(expr: ArrayExpr, normalizeUndefined: boolean) { - return `[${expr.items.map((item) => this.transform(item, normalizeUndefined)).join(', ')}]`; - } - - private literal(expr: LiteralExpr) { - if (expr.$type === StringLiteral) { - return `'${expr.value}'`; - } else { - return expr.value.toString(); - } - } - - private unary(expr: UnaryExpr, normalizeUndefined: boolean) { - const operand = this.transform(expr.operand, normalizeUndefined); - let result = `(${expr.operator} ${operand})`; - if ( - expr.operator === '!' && - this.options.context === ExpressionContext.ValidationRule && - isDataModelFieldReference(expr.operand) - ) { - // in a validation context, we treat unary involving undefined as boolean true - result = this.ensureBooleanTernary(expr.operand, operand, result); - } - return result; - } - - private isModelType(expr: Expression) { - return isDataModel(expr.$resolvedType?.decl); - } - - private binary(expr: BinaryExpr, normalizeUndefined: boolean): string { - let left = this.transform(expr.left, normalizeUndefined); - let right = this.transform(expr.right, normalizeUndefined); - if (this.isModelType(expr.left) && this.isModelType(expr.right)) { - // comparison between model type values, map to id comparison - left = `(${left}?.id ?? null)`; - right = `(${right}?.id ?? null)`; - } - - let _default = `(${left} ${expr.operator} ${right})`; - - if (this.options.context === ExpressionContext.ValidationRule) { - const nullComparison = this.extractNullComparison(expr); - if (nullComparison) { - // null comparison covers both null and undefined - const { fieldRef } = nullComparison; - const field = this.transform(fieldRef, normalizeUndefined); - if (expr.operator === '==') { - _default = `(${field} === null || ${field} === undefined)`; - } else if (expr.operator === '!=') { - _default = `(${field} !== null && ${field} !== undefined)`; - } - } else { - // for other comparisons, in a validation context, - // we treat binary involving undefined as boolean true - if (isDataModelFieldReference(expr.left)) { - _default = this.ensureBooleanTernary(expr.left, left, _default); - } - if (isDataModelFieldReference(expr.right)) { - _default = this.ensureBooleanTernary(expr.right, right, _default); - } - } - } - - return match(expr.operator) - .with('in', () => { - const left = `${this.transform(expr.left, normalizeUndefined)}`; - const right = `${this.transform(expr.right, false)}`; - let result = `${right}?.includes(${left})`; - if (this.options.context === ExpressionContext.ValidationRule) { - // in a validation context, we treat binary involving undefined as boolean true - result = this.ensureBooleanTernary( - expr.left, - left, - this.ensureBooleanTernary(expr.right, right, result) - ); - } else { - result = this.ensureBoolean(result); - } - return result; - }) - .with(P.union('==', '!='), () => { - if (isThisExpr(expr.left) || isThisExpr(expr.right)) { - // map equality comparison with `this` to id comparison - const _this = isThisExpr(expr.left) ? expr.left : expr.right; - const model = _this.$resolvedType?.decl as DataModel; - const idFields = getIdFields(model); - if (!idFields || idFields.length === 0) { - throw new TypeScriptExpressionTransformerError( - `model "${model.name}" does not have an id field` - ); - } - let result = `allFieldsEqual(${this.transform(expr.left, false)}, - ${this.transform(expr.right, false)}, [${idFields.map((f) => "'" + f.name + "'").join(', ')}])`; - if (expr.operator === '!=') { - result = `!${result}`; - } - return result; - } else { - const isLiteralOrEvaluatesToLiteral = (e: Expression) => - isLiteralExpr(e) || - (isInvocationExpr(e) && - (e.function.$refText === 'currentModel' || e.function.$refText === 'currentOperation')); - if (isLiteralOrEvaluatesToLiteral(expr.left) && isLiteralOrEvaluatesToLiteral(expr.right)) { - // resolve trivial comparisons to avoid TS compiler errors - if (expr.operator === '==') { - if (left === right) { - return 'true'; - } else { - return 'false'; - } - } else if (expr.operator === '!=') { - if (left !== right) { - return 'true'; - } else { - return 'false'; - } - } - } - return _default; - } - }) - .with(P.union('?', '!', '^'), (op) => this.collectionPredicate(expr, op, normalizeUndefined)) - .otherwise(() => _default); - } - - private extractNullComparison(expr: BinaryExpr) { - if (expr.operator !== '==' && expr.operator !== '!=') { - return undefined; - } - - if (isDataModelFieldReference(expr.left) && isNullExpr(expr.right)) { - return { fieldRef: expr.left, nullExpr: expr.right }; - } else if (isDataModelFieldReference(expr.right) && isNullExpr(expr.left)) { - return { fieldRef: expr.right, nullExpr: expr.left }; - } else { - return undefined; - } - } - - private collectionPredicate(expr: BinaryExpr, operator: '?' | '!' | '^', normalizeUndefined: boolean) { - const operand = this.transform(expr.left, normalizeUndefined); - const innerTransformer = new TypeScriptExpressionTransformer({ - ...this.options, - isPostGuard: false, - fieldReferenceContext: '_item', - operationContext: this.options.operationContext, - }); - const predicate = innerTransformer.transform(expr.right, normalizeUndefined); - - return match(operator) - .with('?', () => this.ensureBoolean(`(${operand})?.some((_item: any) => ${predicate})`)) - .with('!', () => this.ensureBoolean(`(${operand})?.every((_item: any) => ${predicate})`)) - .with('^', () => `!((${operand})?.some((_item: any) => ${predicate}))`) - .exhaustive(); - } -} diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts deleted file mode 100644 index 9895582c7..000000000 --- a/packages/sdk/src/utils.ts +++ /dev/null @@ -1,732 +0,0 @@ -import { - AstNode, - Attribute, - AttributeParam, - ConfigExpr, - DataModel, - DataModelAttribute, - DataModelField, - DataModelFieldAttribute, - Enum, - EnumField, - Expression, - FunctionDecl, - GeneratorDecl, - InternalAttribute, - isArrayExpr, - isConfigArrayExpr, - isDataModel, - isDataModelField, - isDataSource, - isEnumField, - isExpression, - isGeneratorDecl, - isInvocationExpr, - isLiteralExpr, - isMemberAccessExpr, - isModel, - isObjectExpr, - isReferenceExpr, - isTypeDef, - isTypeDefField, - Model, - Reference, - ReferenceExpr, - TypeDef, - TypeDefField, -} from '@zenstackhq/language/ast'; -import fs from 'node:fs'; -import path from 'path'; -import { ExpressionContext, STD_LIB_MODULE_NAME } from './constants'; -import { PluginError, type PluginDeclaredOptions, type PluginOptions } from './types'; - -/** - * Gets data models in the ZModel schema. - */ -export function getDataModels(model: Model, includeIgnored = false) { - const r = model.declarations.filter((d): d is DataModel => isDataModel(d)); - if (includeIgnored) { - return r; - } else { - return r.filter((model) => !hasAttribute(model, '@@ignore')); - } -} - -/** - * 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)); - if (includeIgnored) { - return r; - } else { - return r.filter((model) => !hasAttribute(model, '@@ignore')); - } -} - -export function resolved(ref: Reference): T { - if (!ref.ref) { - throw new Error(`Reference not resolved: ${ref.$refText}`); - } - return ref.ref; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getLiteral( - expr: Expression | ConfigExpr | undefined -): T | undefined { - switch (expr?.$type) { - case 'ObjectExpr': - return getObjectLiteral(expr); - case 'StringLiteral': - case 'BooleanLiteral': - return expr.value as T; - case 'NumberLiteral': - return parseFloat(expr.value) as T; - default: - return undefined; - } -} - -export function getArray(expr: Expression | ConfigExpr | undefined) { - return isArrayExpr(expr) || isConfigArrayExpr(expr) ? expr.items : undefined; -} - -export function getLiteralArray< - // eslint-disable-next-line @typescript-eslint/no-explicit-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); -} - -export function getObjectLiteral(expr: Expression | ConfigExpr | undefined): T | undefined { - if (!expr || !isObjectExpr(expr)) { - return undefined; - } - const result: Record = {}; - for (const field of expr.fields) { - let fieldValue: unknown; - if (isLiteralExpr(field.value)) { - fieldValue = getLiteral(field.value); - } else if (isArrayExpr(field.value)) { - fieldValue = getLiteralArray(field.value); - } else if (isObjectExpr(field.value)) { - fieldValue = getObjectLiteral(field.value); - } - if (fieldValue === undefined) { - return undefined; - } else { - result[field.name] = fieldValue; - } - } - return result as T; -} - -export function indentString(string: string, count = 4): string { - const indent = ' '; - return string.replace(/^(?!\s*$)/gm, indent.repeat(count)); -} - -export function hasAttribute( - decl: - | DataModel - | TypeDef - | DataModelField - | TypeDefField - | Enum - | EnumField - | FunctionDecl - | Attribute - | AttributeParam, - name: string -) { - return !!getAttribute(decl, name); -} - -export function getAttribute( - decl: - | DataModel - | TypeDef - | DataModelField - | TypeDefField - | Enum - | EnumField - | FunctionDecl - | Attribute - | AttributeParam, - name: string -) { - return (decl.attributes as (DataModelAttribute | DataModelFieldAttribute)[]).find( - (attr) => attr.decl.$refText === name - ); -} - -export function getAttributeArgs( - attr: DataModelAttribute | DataModelFieldAttribute | InternalAttribute -): Record { - const result: Record = {}; - for (const arg of attr.args) { - if (!arg.$resolvedParam) { - continue; - } - result[arg.$resolvedParam.name] = arg.value; - } - return result; -} - -export function getAttributeArg( - attr: DataModelAttribute | DataModelFieldAttribute | InternalAttribute, - name: string -): Expression | undefined { - for (const arg of attr.args) { - if (arg.$resolvedParam?.name === name) { - return arg.value; - } - } - return undefined; -} - -export function getAttributeArgLiteral( - attr: DataModelAttribute | DataModelFieldAttribute, - name: string -): T | undefined { - for (const arg of attr.args) { - if (arg.$resolvedParam?.name === name) { - return getLiteral(arg.value); - } - } - return undefined; -} - -export function isEnumFieldReference(node: AstNode): node is ReferenceExpr { - return isReferenceExpr(node) && isEnumField(node.target.ref); -} - -export function isDataModelFieldReference(node: AstNode): node is ReferenceExpr { - return isReferenceExpr(node) && isDataModelField(node.target.ref); -} - -/** - * 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)]; - - for (const modelToCheck of modelsToCheck) { - const idAttr = modelToCheck.attributes.find((attr) => attr.decl.$refText === '@@id'); - if (!idAttr) { - continue; - } - const fieldsArg = idAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); - if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { - continue; - } - - return fieldsArg.value.items - .filter((item): item is ReferenceExpr => isReferenceExpr(item)) - .map((item) => resolved(item.target) as DataModelField); - } - - return []; -} - -/** - * 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)]; - - for (const modelToCheck of modelsToCheck) { - const uniqueAttr = modelToCheck.attributes.find((attr) => attr.decl.$refText === '@@unique'); - if (!uniqueAttr) { - continue; - } - const fieldsArg = uniqueAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); - if (!fieldsArg || !isArrayExpr(fieldsArg.value)) { - continue; - } - - return fieldsArg.value.items - .filter((item): item is ReferenceExpr => isReferenceExpr(item)) - .map((item) => resolved(item.target) as DataModelField); - } - - return []; -} - -/** - * Returns if the given field is declared as an id field. - */ -export function isIdField(field: DataModelField) { - // field-level @id attribute - if (hasAttribute(field, '@id')) { - return true; - } - - // NOTE: we have to use name to match fields because the fields - // may be inherited from an abstract base and have cloned identities - - const model = field.$container as DataModel; - - // model-level @@id attribute with a list of fields - const modelLevelIds = getModelIdFields(model); - if (modelLevelIds.map((f) => f.name).includes(field.name)) { - return true; - } - - 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')); - if (firstUniqueField) { - return firstUniqueField.name === field.name; - } - - // last, the first model level @@unique can be used as id - const modelLevelUnique = getModelUniqueFields(model); - if (modelLevelUnique.map((f) => f.name).includes(field.name)) { - return true; - } - - return false; -} - -/** - * Returns if the given field is a relation field. - */ -export function isRelationshipField(field: DataModelField) { - return isDataModel(field.type.reference?.ref); -} - -/** - * Returns if the given field is a relation foreign key field. - */ -export function isForeignKeyField(field: DataModelField) { - const model = field.$container as DataModel; - return model.fields.some((f) => { - // find @relation attribute - const relAttr = f.attributes.find((attr) => attr.decl.ref?.name === '@relation'); - if (relAttr) { - // find "fields" arg - const fieldsArg = relAttr.args.find((a) => a.$resolvedParam?.name === 'fields'); - - if (fieldsArg && isArrayExpr(fieldsArg.value)) { - // find a matching field reference - return fieldsArg.value.items.some((item): item is ReferenceExpr => { - if (isReferenceExpr(item)) { - return item.target.ref === field; - } else { - return false; - } - }); - } - } - return false; - }); -} - -/** - * Gets the foreign key-id field pairs from the given relation field. - */ -export function getRelationKeyPairs(relationField: DataModelField) { - if (!isRelationshipField(relationField)) { - return []; - } - - const relAttr = relationField.attributes.find((attr) => attr.decl.ref?.name === '@relation'); - if (relAttr) { - // find "fields" arg - const fieldsArg = getAttributeArg(relAttr, 'fields'); - let fkFields: DataModelField[]; - if (fieldsArg && isArrayExpr(fieldsArg)) { - fkFields = fieldsArg.items - .filter((item): item is ReferenceExpr => isReferenceExpr(item)) - .map((item) => item.target.ref as DataModelField); - } else { - return []; - } - - // find "references" arg - const referencesArg = getAttributeArg(relAttr, 'references'); - let idFields: DataModelField[]; - if (referencesArg && isArrayExpr(referencesArg)) { - idFields = referencesArg.items - .filter((item): item is ReferenceExpr => isReferenceExpr(item)) - .map((item) => item.target.ref as DataModelField); - } else { - return []; - } - - if (idFields.length !== fkFields.length) { - throw new Error(`Relation's references arg and fields are must have equal length`); - } - - return idFields.map((idField, i) => ({ id: idField, foreignKey: fkFields[i] })); - } - - return []; -} - -/** - * Gets the relation field of the given foreign key field. - */ -export function getRelationField(fkField: DataModelField) { - const model = fkField.$container as DataModel; - return model.fields.find((f) => { - const relAttr = f.attributes.find((attr) => attr.decl.ref?.name === '@relation'); - if (relAttr) { - const fieldsArg = getAttributeArg(relAttr, 'fields'); - if (fieldsArg && isArrayExpr(fieldsArg)) { - return fieldsArg.items.some((item) => isReferenceExpr(item) && item.target.ref === fkField); - } - } - return false; - }); -} - -/** - * Gets the foreign key fields of the given relation field. - */ -export function getForeignKeyFields(relationField: DataModelField) { - return getRelationKeyPairs(relationField).map((pair) => pair.foreignKey); -} - -export function resolvePath(_path: string, options: Pick) { - if (path.isAbsolute(_path)) { - return _path; - } else { - return path.resolve(path.dirname(options.schemaPath), _path); - } -} - -export function requireOption(options: PluginDeclaredOptions, name: string, pluginName: string): T { - const value = options[name]; - if (value === undefined) { - throw new PluginError(pluginName, `required option "${name}" is not provided`); - } - return value as T; -} - -export function parseOptionAsStrings(options: PluginDeclaredOptions, optionName: string, pluginName: string) { - const value = options[optionName]; - if (value === undefined) { - return undefined; - } else if (typeof value === 'string') { - // comma separated string - return value - .split(',') - .filter((i) => !!i) - .map((i) => i.trim()); - } else if (Array.isArray(value) && value.every((i) => typeof i === 'string')) { - // string array - return value as string[]; - } else { - throw new PluginError( - pluginName, - `Invalid "${optionName}" option: must be a comma-separated string or an array of strings` - ); - } -} - -export function getFunctionExpressionContext(funcDecl: FunctionDecl) { - const funcAllowedContext: 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); - } - }); - } - } - return funcAllowedContext; -} - -export function isFutureExpr(node: AstNode) { - return isInvocationExpr(node) && node.function.ref?.name === 'future' && isFromStdlib(node.function.ref); -} - -export function isAuthInvocation(node: AstNode) { - return isInvocationExpr(node) && node.function.ref?.name === 'auth' && isFromStdlib(node.function.ref); -} - -export function isFromStdlib(node: AstNode) { - const model = getContainingModel(node); - return !!model && !!model.$document && model.$document.uri.path.endsWith(STD_LIB_MODULE_NAME); -} - -export function getContainingModel(node: AstNode | undefined): Model | null { - if (!node) { - return null; - } - return isModel(node) ? node : getContainingModel(node.$container); -} - -export function getPreviewFeatures(model: Model) { - const jsGenerator = model.declarations.find( - (d) => - isGeneratorDecl(d) && - d.fields.some( - (f) => - f.name === 'provider' && - (getLiteral(f.value) === 'prisma-client-js' || - getLiteral(f.value) === 'prisma-client') - ) - ) as GeneratorDecl | undefined; - - if (jsGenerator) { - const previewFeaturesField = jsGenerator.fields.find((f) => f.name === 'previewFeatures'); - if (previewFeaturesField) { - return getLiteralArray(previewFeaturesField.value); - } - } - - return [] as string[]; -} - -export function getAuthDecl(decls: (DataModel | TypeDef)[]) { - let authModel = decls.find((m) => hasAttribute(m, '@@auth')); - if (!authModel) { - authModel = decls.find((m) => m.name === 'User'); - } - return authModel; -} - -export function isDelegateModel(node: AstNode) { - return isDataModel(node) && hasAttribute(node, '@@delegate'); -} - -export function isDiscriminatorField(field: DataModelField) { - if (!isDataModel(field.$container)) { - return false; - } - const model = field.$inheritedFrom ?? field.$container; - const delegateAttr = getAttribute(model, '@@delegate'); - if (!delegateAttr) { - return false; - } - const arg = delegateAttr.args[0]?.value; - return isDataModelFieldReference(arg) && arg.target.$refText === field.name; -} - -export function getIdFields(decl: DataModel | TypeDef) { - const fields = isDataModel(decl) ? getModelFieldsWithBases(decl) : decl.fields; - const fieldLevelId = fields.find((f) => f.attributes.some((attr) => attr.decl.$refText === '@id')); - if (fieldLevelId) { - return [fieldLevelId]; - } else { - // get model level @@id attribute - const modelIdAttr = decl.attributes.find((attr) => attr.decl?.ref?.name === '@@id'); - if (modelIdAttr) { - // get fields referenced in the attribute: @@id([field1, field2]]) - if (!isArrayExpr(modelIdAttr.args[0].value)) { - return []; - } - const argValue = modelIdAttr.args[0].value; - return argValue.items - .filter((expr): expr is ReferenceExpr => isReferenceExpr(expr) && !!getFieldReference(expr)) - .map((expr) => expr.target.ref as DataModelField); - } - } - return []; -} - -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))) { - return expr.member.ref; - } else { - return undefined; - } -} - -export function getModelFieldsWithBases(model: DataModel, includeDelegate = true) { - if (model.$baseMerged) { - return model.fields; - } else { - return [...model.fields, ...getRecursiveBases(model, includeDelegate).flatMap((base) => base.fields)]; - } -} - -export function getRecursiveBases( - dataModel: DataModel, - includeDelegate = true, - seen = new Set() -): DataModel[] { - const result: DataModel[] = []; - if (seen.has(dataModel)) { - return result; - } - seen.add(dataModel); - dataModel.superTypes.forEach((superType) => { - const baseDecl = superType.ref; - if (baseDecl) { - if (!includeDelegate && isDelegateModel(baseDecl)) { - return; - } - result.push(baseDecl); - result.push(...getRecursiveBases(baseDecl, includeDelegate, seen)); - } - }); - return result; -} - -export function ensureEmptyDir(dir: string) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - return; - } - - const stats = fs.statSync(dir); - if (stats.isDirectory()) { - fs.rmSync(dir, { recursive: true }); - fs.mkdirSync(dir, { recursive: true }); - } else { - throw new Error(`Path "${dir}" already exists and is not a directory`); - } -} - -/** - * Gets the data source provider from the given model. - */ -export function getDataSourceProvider(model: Model) { - const dataSource = model.declarations.find(isDataSource); - if (!dataSource) { - return undefined; - } - const provider = dataSource?.fields.find((f) => f.name === 'provider'); - if (!provider) { - return undefined; - } - return getLiteral(provider.value); -} - -/** - * Finds the original delegate base model that defines the given field. - */ -export function getInheritedFromDelegate(field: DataModelField) { - if (!field.$inheritedFrom) { - return undefined; - } - - // find the original base delegate model that defines this field, - // use `findLast` to start from the uppermost base - const bases = getRecursiveBases(field.$container as DataModel, true); - const foundBase = bases.findLast((base) => base.fields.some((f) => f.name === field.name) && isDelegateModel(base)); - return foundBase; -} - -/** - * Gets the inheritance chain from "from" to "to", excluding them. - */ -export function getInheritanceChain(from: DataModel, to: DataModel): DataModel[] | undefined { - if (from === to) { - return []; - } - - for (const base of from.superTypes) { - if (base.ref === to) { - return []; - } - const path = getInheritanceChain(base.ref!, to); - if (path) { - return [base.ref as DataModel, ...path]; - } - } - - return undefined; -} - -/** - * Get the opposite side of a relation field. - */ -export function getRelationBackLink(field: DataModelField) { - if (!field.type.reference?.ref || !isDataModel(field.type.reference?.ref)) { - return undefined; - } - - const relName = getRelationName(field); - - let sourceModel: DataModel; - if (field.$inheritedFrom && isDelegateModel(field.$inheritedFrom)) { - // field is inherited from a delegate model, use it as the source - sourceModel = field.$inheritedFrom; - } else { - // otherwise use the field's container model as the source - sourceModel = field.$container as DataModel; - } - - const targetModel = field.type.reference.ref as DataModel; - - const sameField = (f1: DataModelField, f2: DataModelField) => { - // for fields inherited from a delegate model, always use - // the base to compare - const parent1 = f1.$inheritedFrom ?? f1.$container; - const parent2 = f2.$inheritedFrom ?? f2.$container; - return f1.name === f2.name && parent1 === parent2; - }; - - for (const otherField of targetModel.fields) { - if (sameField(otherField, field)) { - // backlink field is never self - continue; - } - if (otherField.type.reference?.ref === sourceModel) { - if (relName) { - const otherRelName = getRelationName(otherField); - if (relName === otherRelName) { - return otherField; - } - } else { - return otherField; - } - } - } - return undefined; -} - -/** - * Get the relation name of a relation field. - */ -export function getRelationName(field: DataModelField) { - const relAttr = getAttribute(field, '@relation'); - if (!relAttr) { - return undefined; - } - return getAttributeArgLiteral(relAttr, 'name'); -} - -export function getPrismaClientGenerator(model: Model) { - const decl = model.declarations.find( - (d): d is GeneratorDecl => - isGeneratorDecl(d) && - d.fields.some( - (f) => - f.name === 'provider' && - (getLiteral(f.value) === 'prisma-client-js' || - getLiteral(f.value) === 'prisma-client') - ) - ); - if (!decl) { - return undefined; - } - - const provider = getLiteral(decl.fields.find((f) => f.name === 'provider')?.value); - return { - name: decl.name, - output: getLiteral(decl.fields.find((f) => f.name === 'output')?.value), - previewFeatures: getLiteralArray(decl.fields.find((f) => f.name === 'previewFeatures')?.value), - provider, - isNewGenerator: provider === 'prisma-client', - }; -} diff --git a/packages/sdk/src/validation.ts b/packages/sdk/src/validation.ts deleted file mode 100644 index 1c0923f63..000000000 --- a/packages/sdk/src/validation.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - isDataModel, - isTypeDef, - type DataModel, - type DataModelAttribute, - type DataModelFieldAttribute, - type TypeDef, -} from './ast'; - -export function isValidationAttribute(attr: DataModelAttribute | DataModelFieldAttribute) { - return attr.decl.ref?.attributes.some((attr) => attr.decl.$refText === '@@@validation'); -} - -/** - * Returns if the given model contains any data validation rules (both at the model - * level and at the field level). - */ -export function hasValidationAttributes( - decl: DataModel | TypeDef, - seen: Set = new Set() -): boolean { - if (seen.has(decl)) { - return false; - } - seen.add(decl); - - if (isDataModel(decl)) { - if (decl.attributes.some((attr) => isValidationAttribute(attr))) { - return true; - } - } - - if ( - decl.fields.some((field) => { - if (isTypeDef(field.type.reference?.ref)) { - return hasValidationAttributes(field.type.reference?.ref, seen); - } else { - return field.attributes.some((attr) => isValidationAttribute(attr)); - } - }) - ) { - return true; - } - - return false; -} diff --git a/packages/sdk/src/zmodel-code-generator.ts b/packages/sdk/src/zmodel-code-generator.ts deleted file mode 100644 index e64163a99..000000000 --- a/packages/sdk/src/zmodel-code-generator.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { - Argument, - ArrayExpr, - AstNode, - Attribute, - AttributeArg, - AttributeParam, - AttributeParamType, - BinaryExpr, - BinaryExprOperatorPriority, - BooleanLiteral, - ConfigArrayExpr, - ConfigField, - ConfigInvocationExpr, - DataModel, - DataModelAttribute, - DataModelField, - DataModelFieldAttribute, - DataModelFieldType, - DataSource, - Enum, - EnumField, - FieldInitializer, - FunctionDecl, - FunctionParam, - FunctionParamType, - GeneratorDecl, - InvocationExpr, - LiteralExpr, - MemberAccessExpr, - Model, - NullExpr, - NumberLiteral, - ObjectExpr, - Plugin, - PluginField, - ReferenceArg, - ReferenceExpr, - StringLiteral, - ThisExpr, - UnaryExpr, - TypeDef, - TypeDefField, - TypeDefFieldType, -} from './ast'; -import { resolved } from './utils'; - -/** - * Options for the generator. - */ -export interface ZModelCodeOptions { - binaryExprNumberOfSpaces: number; - unaryExprNumberOfSpaces: number; - indent: number; - quote: 'single' | 'double'; -} - -// a registry of generation handlers marked with @gen -const generationHandlers = new Map(); - -// generation handler decorator -function gen(name: string) { - return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { - if (!generationHandlers.get(name)) { - generationHandlers.set(name, descriptor); - } - return descriptor; - }; -} - -/** - * Generates ZModel source code from AST. - */ -export class ZModelCodeGenerator { - private readonly options: ZModelCodeOptions; - - constructor(options?: Partial) { - this.options = { - binaryExprNumberOfSpaces: options?.binaryExprNumberOfSpaces ?? 1, - unaryExprNumberOfSpaces: options?.unaryExprNumberOfSpaces ?? 0, - indent: options?.indent ?? 4, - quote: options?.quote ?? 'single', - }; - } - - /** - * Generates ZModel source code from AST. - */ - generate(ast: AstNode): string { - const handler = generationHandlers.get(ast.$type); - if (!handler) { - throw new Error(`No generation handler found for ${ast.$type}`); - } - return handler.value.call(this, ast); - } - - @gen(Model) - private _generateModel(ast: Model) { - return ast.declarations.map((d) => this.generate(d)).join('\n\n'); - } - - @gen(DataSource) - private _generateDataSource(ast: DataSource) { - return `datasource ${ast.name} { -${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} -}`; - } - - @gen(Enum) - private _generateEnum(ast: Enum) { - return `enum ${ast.name} { -${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(' ') : '' - }`; - } - - @gen(GeneratorDecl) - private _generateGenerator(ast: GeneratorDecl) { - return `generator ${ast.name} { -${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} -}`; - } - - @gen(ConfigField) - private _generateConfigField(ast: ConfigField) { - return `${ast.name} = ${this.generate(ast.value)}`; - } - - @gen(ConfigArrayExpr) - private _generateConfigArrayExpr(ast: ConfigArrayExpr) { - return `[${ast.items.map((x) => this.generate(x)).join(', ')}]`; - } - - @gen(ConfigInvocationExpr) - private _generateConfigInvocationExpr(ast: ConfigInvocationExpr) { - if (ast.args.length === 0) { - return ast.name; - } else { - return `${ast.name}(${ast.args - .map((x) => (x.name ? x.name + ': ' : '') + this.generate(x.value)) - .join(', ')})`; - } - } - - @gen(Plugin) - private _generatePlugin(ast: Plugin) { - return `plugin ${ast.name} { -${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} -}`; - } - - @gen(PluginField) - private _generatePluginField(ast: PluginField) { - return `${ast.name} = ${this.generate(ast.value)}`; - } - - @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(', ') : '' - } { -${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') - : '' - } -}`; - } - - @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(' ') : '' - }`; - } - - private fieldType(type: DataModelFieldType | TypeDefFieldType) { - 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 ? '?' : ''}`; - } - - @gen(DataModelAttribute) - private _generateDataModelAttribute(ast: DataModelAttribute) { - return this.attribute(ast); - } - - @gen(DataModelFieldAttribute) - private _generateDataModelFieldAttribute(ast: DataModelFieldAttribute) { - return this.attribute(ast); - } - - private attribute(ast: DataModelAttribute | DataModelFieldAttribute) { - const args = ast.args.length ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` : ''; - return `${resolved(ast.decl).name}${args}`; - } - - @gen(AttributeArg) - private _generateAttributeArg(ast: AttributeArg) { - if (ast.name) { - return `${ast.name}: ${this.generate(ast.value)}`; - } else { - return this.generate(ast.value); - } - } - - @gen(ObjectExpr) - private _generateObjectExpr(ast: ObjectExpr) { - return `{ ${ast.fields.map((field) => this.objectField(field)).join(', ')} }`; - } - - private objectField(field: FieldInitializer) { - return `${field.name}: ${this.generate(field.value)}`; - } - - @gen(ArrayExpr) - private _generateArrayExpr(ast: ArrayExpr) { - return `[${ast.items.map((item) => this.generate(item)).join(', ')}]`; - } - - @gen(StringLiteral) - private _generateLiteralExpr(ast: LiteralExpr) { - return this.options.quote === 'single' ? `'${ast.value}'` : `"${ast.value}"`; - } - - @gen(NumberLiteral) - private _generateNumberLiteral(ast: NumberLiteral) { - return ast.value.toString(); - } - - @gen(BooleanLiteral) - private _generateBooleanLiteral(ast: BooleanLiteral) { - return ast.value.toString(); - } - - @gen(UnaryExpr) - private _generateUnaryExpr(ast: UnaryExpr) { - 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 rightExpr = this.generate(ast.right); - - const { left: isLeftParenthesis, right: isRightParenthesis } = this.isParenthesesNeededForBinaryExpr(ast); - - return `${isLeftParenthesis ? '(' : ''}${this.generate(ast.left)}${isLeftParenthesis ? ')' : ''}${ - isCollectionPredicate ? '' : this.binaryExprSpace - }${operator}${isCollectionPredicate ? '' : this.binaryExprSpace}${isRightParenthesis ? '(' : ''}${ - isCollectionPredicate ? `[${rightExpr}]` : rightExpr - }${isRightParenthesis ? ')' : ''}`; - } - - @gen(ReferenceExpr) - private _generateReferenceExpr(ast: ReferenceExpr) { - const args = ast.args.length ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` : ''; - return `${ast.target.ref?.name}${args}`; - } - - @gen(ReferenceArg) - private _generateReferenceArg(ast: ReferenceArg) { - return `${ast.name}:${this.generate(ast.value)}`; - } - - @gen(MemberAccessExpr) - private _generateMemberExpr(ast: MemberAccessExpr) { - return `${this.generate(ast.operand)}.${ast.member.ref?.name}`; - } - - @gen(InvocationExpr) - private _generateInvocationExpr(ast: InvocationExpr) { - return `${ast.function.ref?.name}(${ast.args.map((x) => this.argument(x)).join(', ')})`; - } - - @gen(NullExpr) - private _generateNullExpr() { - return 'null'; - } - - @gen(ThisExpr) - private _generateThisExpr() { - return 'this'; - } - - @gen(Attribute) - private _generateAttribute(ast: Attribute) { - 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)}`; - } - - @gen(AttributeParamType) - private _generateAttributeParamType(ast: AttributeParamType) { - 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(', ')}) ${ - ast.returnType ? ': ' + this.generate(ast.returnType) : '' - } {}`; - } - - @gen(FunctionParam) - private _generateFunctionParam(ast: FunctionParam) { - return `${ast.name}: ${this.generate(ast.type)}`; - } - - @gen(FunctionParamType) - private _generateFunctionParamType(ast: FunctionParamType) { - return `${ast.type ?? ast.reference?.$refText}${ast.array ? '[]' : ''}`; - } - - @gen(TypeDef) - private _generateTypeDef(ast: TypeDef) { - 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') - : '' - } -}`; - } - - @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(' ') : '' - }`; - } - - private argument(ast: Argument) { - return this.generate(ast.value); - } - - private get binaryExprSpace() { - return ' '.repeat(this.options.binaryExprNumberOfSpaces); - } - - private get unaryExprSpace() { - return ' '.repeat(this.options.unaryExprNumberOfSpaces); - } - - private get indent() { - return ' '.repeat(this.options.indent); - } - - private isParenthesesNeededForBinaryExpr(ast: BinaryExpr): { left: boolean; right: boolean } { - const result = { left: false, right: false }; - const operator = ast.operator; - const isCollectionPredicate = this.isCollectionPredicateOperator(operator); - - const currentPriority = BinaryExprOperatorPriority[operator]; - - if ( - ast.left.$type === BinaryExpr && - BinaryExprOperatorPriority[(ast.left as BinaryExpr)['operator']] < currentPriority - ) { - result.left = true; - } - /** - * 1 collection predicate operator has [] around the right operand, no need to add parenthesis. - * 2 grammar is left associative, so if the right operand has the same priority still need to add parenthesis. - **/ - if ( - !isCollectionPredicate && - ast.right.$type === BinaryExpr && - BinaryExprOperatorPriority[(ast.right as BinaryExpr)['operator']] <= currentPriority - ) { - result.right = true; - } - - return result; - } - - private isCollectionPredicateOperator(op: BinaryExpr['operator']) { - return ['?', '!', '^'].includes(op); - } -} diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/server/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/server/LICENSE b/packages/server/LICENSE deleted file mode 120000 index 30cff7403..000000000 --- a/packages/server/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/server/README.md b/packages/server/README.md deleted file mode 100644 index b4b4ffb4c..000000000 --- a/packages/server/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ZenStack Server Adapters - -This package provides adapters and utilities for integrating with popular Node.js servers, including Express, Fastify, and Nest.js. - -Visit [Homepage](https://zenstack.dev) for more details. diff --git a/packages/server/jest.config.ts b/packages/server/jest.config.ts deleted file mode 120000 index a12d812fd..000000000 --- a/packages/server/jest.config.ts +++ /dev/null @@ -1 +0,0 @@ -../../jest.config.ts \ No newline at end of file diff --git a/packages/server/src/api/base.ts b/packages/server/src/api/base.ts deleted file mode 100644 index 6b9dbfbbd..000000000 --- a/packages/server/src/api/base.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { DbClientContract, ModelMeta, ZodSchemas } from '@zenstackhq/runtime'; -import { getDefaultModelMeta } from '../shared'; -import type { LoggerConfig } from '../types'; - -/** - * API request context - */ -export type RequestContext = { - /** - * The PrismaClient instance - */ - prisma: DbClientContract; - - /** - * The HTTP method - */ - method: string; - - /** - * The request endpoint path (excluding any prefix) - */ - path: string; - - /** - * The query parameters - */ - query?: Record; - - /** - * The request body object - */ - requestBody?: unknown; - - /** - * Model metadata. By default loaded from the @see loadPath path. You can pass - * it in explicitly to override. - */ - modelMeta?: ModelMeta; - - /** - * Zod schemas for validating create and update payloads. By default loaded from - * the @see loadPath path. You can pass it in explicitly to override. - */ - zodSchemas?: ZodSchemas; - - /** - * Logging configuration. Set to `null` to disable logging. - * If unset or set to `undefined`, log will be output to console. - */ - logger?: LoggerConfig; -}; - -/** - * Base class for API handlers - */ -export abstract class APIHandlerBase { - // model meta loaded from default location - protected readonly defaultModelMeta: ModelMeta | undefined; - - constructor() { - try { - this.defaultModelMeta = getDefaultModelMeta(); - } catch { - // noop - } - } -} diff --git a/packages/server/src/elysia/handler.ts b/packages/server/src/elysia/handler.ts deleted file mode 100644 index f3bb72b1d..000000000 --- a/packages/server/src/elysia/handler.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DbClientContract } from '@zenstackhq/runtime'; -import { Elysia, Context as ElysiaContext } from 'elysia'; -import { RPCApiHandler } from '../api'; -import { loadAssets } from '../shared'; -import { AdapterBaseOptions } from '../types'; - -/** - * Options for initializing an Elysia middleware. - */ -export interface ElysiaOptions extends AdapterBaseOptions { - /** - * Callback method for getting a Prisma instance for the given request context. - */ - getPrisma: (context: ElysiaContext) => Promise | unknown; - /** - * Optional base path to strip from the request path before passing to the API handler. - */ - basePath?: string; -} - -/** - * Creates an Elysia middleware handler for ZenStack. - * This handler provides automatic CRUD APIs through Elysia's routing system. - */ -export function createElysiaHandler(options: ElysiaOptions) { - const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler ?? RPCApiHandler(); - - return async (app: Elysia) => { - app.all('/*', async (ctx: ElysiaContext) => { - const { request, body, set } = ctx; - const prisma = (await options.getPrisma(ctx)) as DbClientContract; - if (!prisma) { - set.status = 500; - return { - message: 'unable to get prisma from request context', - }; - } - - const url = new URL(request.url); - const query = Object.fromEntries(url.searchParams); - let path = url.pathname; - - if (options.basePath && path.startsWith(options.basePath)) { - path = path.slice(options.basePath.length); - if (!path.startsWith('/')) { - path = '/' + path; - } - } - - if (!path || path === '/') { - set.status = 400; - return { - message: 'missing path parameter', - }; - } - - try { - const r = await requestHandler({ - method: request.method, - path, - query, - requestBody: body, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - - set.status = r.status; - return r.body; - } catch (err) { - set.status = 500; - return { - message: 'An internal server error occurred', - }; - } - }); - - return app; - }; -} diff --git a/packages/server/src/elysia/index.ts b/packages/server/src/elysia/index.ts deleted file mode 100644 index bc0d7e165..000000000 --- a/packages/server/src/elysia/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './handler'; \ No newline at end of file diff --git a/packages/server/src/express/index.ts b/packages/server/src/express/index.ts deleted file mode 100644 index 1def0479b..000000000 --- a/packages/server/src/express/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ZenStackMiddleware } from './middleware'; -export * from './middleware'; diff --git a/packages/server/src/express/middleware.ts b/packages/server/src/express/middleware.ts deleted file mode 100644 index 67a185704..000000000 --- a/packages/server/src/express/middleware.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { DbClientContract } from '@zenstackhq/runtime'; -import type { Handler, Request, Response } from 'express'; -import { RPCApiHandler } from '../api/rpc'; -import { loadAssets } from '../shared'; -import { AdapterBaseOptions } from '../types'; - -/** - * Express middleware options - */ -export interface MiddlewareOptions extends AdapterBaseOptions { - /** - * Callback for getting a PrismaClient for the given request - */ - getPrisma: (req: Request, res: Response) => unknown | Promise; - - /** - * Controls if the middleware directly sends a response. If set to false, - * the response is stored in the `res.locals` object and then the middleware - * calls the `next()` function to pass the control to the next middleware. - * Subsequent middleware or request handlers need to make sure to send - * a response. - * - * Defaults to true; - */ - sendResponse?: boolean; -} - -/** - * Creates an Express middleware for handling CRUD requests. - */ -const factory = (options: MiddlewareOptions): Handler => { - const { modelMeta, zodSchemas } = loadAssets(options); - - const requestHandler = options.handler || RPCApiHandler(); - - return async (request, response, next) => { - const prisma = (await options.getPrisma(request, response)) as DbClientContract; - const { sendResponse } = options; - - if (sendResponse === false && !prisma) { - throw new Error('unable to get prisma from request context'); - } - - if (!prisma) { - return response.status(500).json({ message: 'unable to get prisma from request context' }); - } - - // express converts query parameters with square brackets into object - // e.g.: filter[foo]=bar is parsed to { filter: { foo: 'bar' } } - // we need to revert this behavior and reconstruct params from original URL - const url = request.protocol + '://' + request.get('host') + request.originalUrl; - const searchParams = new URL(url).searchParams; - const query = Object.fromEntries(searchParams); - - try { - const r = await requestHandler({ - method: request.method, - path: request.path, - query, - requestBody: request.body, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - if (sendResponse === false) { - // attach response and pass control to the next middleware - response.locals = { - status: r.status, - body: r.body, - }; - return next(); - } - return response.status(r.status).json(r.body); - } catch (err) { - if (sendResponse === false) { - throw err; - } - return response.status(500).json({ message: `An unhandled error occurred: ${err}` }); - } - }; -}; - -export default factory; - -export { factory as ZenStackMiddleware }; diff --git a/packages/server/src/fastify/index.ts b/packages/server/src/fastify/index.ts deleted file mode 100644 index 486b76c64..000000000 --- a/packages/server/src/fastify/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ZenStackFastifyPlugin } from './plugin'; -export * from './plugin'; diff --git a/packages/server/src/fastify/plugin.ts b/packages/server/src/fastify/plugin.ts deleted file mode 100644 index a69d9ac98..000000000 --- a/packages/server/src/fastify/plugin.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { DbClientContract } from '@zenstackhq/runtime'; -import { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify'; -import fp from 'fastify-plugin'; -import RPCApiHandler from '../api/rpc'; -import { logInfo } from '../api/utils'; -import { loadAssets } from '../shared'; -import { AdapterBaseOptions } from '../types'; - -/** - * Fastify plugin options - */ -export interface PluginOptions extends AdapterBaseOptions { - /** - * Url prefix, e.g.: /api - */ - prefix: string; - - /** - * Callback for getting a PrismaClient for the given request - */ - getPrisma: (request: FastifyRequest, reply: FastifyReply) => unknown | Promise; -} - -/** - * Fastify plugin for handling CRUD requests. - */ -const pluginHandler: FastifyPluginCallback = (fastify, options, done) => { - const prefix = options.prefix ?? ''; - logInfo(options.logger, `ZenStackPlugin installing routes at prefix: ${prefix}`); - - const { modelMeta, zodSchemas } = loadAssets(options); - - const requestHandler = options.handler ?? RPCApiHandler(); - - fastify.all(`${prefix}/*`, async (request, reply) => { - const prisma = (await options.getPrisma(request, reply)) as DbClientContract; - if (!prisma) { - reply.status(500).send({ message: 'unable to get prisma from request context' }); - return reply; - } - - try { - const response = await requestHandler({ - method: request.method, - path: (request.params as any)['*'], - query: request.query as Record, - requestBody: request.body, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - reply.status(response.status).send(response.body); - } catch (err) { - reply.status(500).send({ message: `An unhandled error occurred: ${err}` }); - } - - return reply; - }); - - done(); -}; - -const plugin = fp(pluginHandler); - -export default plugin; - -export { plugin as ZenStackFastifyPlugin }; diff --git a/packages/server/src/hono/handler.ts b/packages/server/src/hono/handler.ts deleted file mode 100644 index 45c104197..000000000 --- a/packages/server/src/hono/handler.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { DbClientContract } from '@zenstackhq/runtime'; -import { Context, MiddlewareHandler } from 'hono'; -import { StatusCode } from 'hono/utils/http-status'; -import { RPCApiHandler } from '../api'; -import { loadAssets } from '../shared'; -import { AdapterBaseOptions } from '../types'; - -/** - * Options for initializing a Hono middleware. - */ -export interface HonoOptions extends AdapterBaseOptions { - /** - * Callback method for getting a Prisma instance for the given request. - */ - getPrisma: (ctx: Context) => Promise | unknown; -} - -export function createHonoHandler(options: HonoOptions): MiddlewareHandler { - const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler ?? RPCApiHandler(); - - return async (ctx) => { - const prisma = (await options.getPrisma(ctx)) as DbClientContract; - if (!prisma) { - return ctx.json({ message: 'unable to get prisma from request context' }, 500); - } - - const url = new URL(ctx.req.url); - const query = Object.fromEntries(url.searchParams); - - const path = ctx.req.path.substring(ctx.req.routePath.length - 1); - - if (!path) { - return ctx.json({ message: 'missing path parameter' }, 400); - } - - let requestBody: unknown; - if (ctx.req.raw.body) { - try { - requestBody = await ctx.req.json(); - } catch { - // noop - } - } - - try { - const r = await requestHandler({ - method: ctx.req.method, - path, - query, - requestBody, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - return ctx.json(r.body as object, r.status as StatusCode); - } catch (err) { - return ctx.json({ message: `An unhandled error occurred: ${err}` }, 500); - } - }; -} diff --git a/packages/server/src/hono/index.ts b/packages/server/src/hono/index.ts deleted file mode 100644 index 68ae53f6c..000000000 --- a/packages/server/src/hono/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './handler'; diff --git a/packages/server/src/nestjs/api-handler.service.ts b/packages/server/src/nestjs/api-handler.service.ts deleted file mode 100644 index 1b157e998..000000000 --- a/packages/server/src/nestjs/api-handler.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { DbClientContract } from '@zenstackhq/runtime'; -import { HttpException, Inject, Injectable, Scope } from "@nestjs/common"; -import { HttpAdapterHost, REQUEST } from "@nestjs/core"; -import { loadAssets } from "../shared"; -import { RPCApiHandler } from '../api/rpc'; -import { ENHANCED_PRISMA } from "./zenstack.constants"; -import { ApiHandlerOptions } from './interfaces'; - -/** - * The ZenStack API handler service for NestJS. The service is used to handle API requests - * and forward them to the ZenStack API handler. It is platform agnostic and can be used - * with any HTTP adapter. - */ -@Injectable({ scope: Scope.REQUEST }) -export class ApiHandlerService { - constructor( - private readonly httpAdapterHost: HttpAdapterHost, - @Inject(ENHANCED_PRISMA) private readonly prisma: DbClientContract, - @Inject(REQUEST) private readonly request: unknown - ) { } - - async handleRequest(options?: ApiHandlerOptions): Promise { - const { modelMeta, zodSchemas } = loadAssets(options || {}); - const requestHandler = options?.handler || RPCApiHandler(); - const hostname = this.httpAdapterHost.httpAdapter.getRequestHostname(this.request); - const requestUrl = this.httpAdapterHost.httpAdapter.getRequestUrl(this.request); - // prefix with http:// to make a valid url accepted by URL constructor - const url = new URL(`http://${hostname}${requestUrl}`); - const method = this.httpAdapterHost.httpAdapter.getRequestMethod(this.request); - const path = options?.baseUrl && url.pathname.startsWith(options.baseUrl) ? url.pathname.slice(options.baseUrl.length) : url.pathname; - const searchParams = url.searchParams; - const query = Object.fromEntries(searchParams); - const requestBody = (this.request as { body: unknown }).body; - - const response = await requestHandler({ - method, - path, - query, - requestBody, - prisma: this.prisma, - modelMeta, - zodSchemas, - logger: options?.logger, - }); - - // handle handler error - // if response code >= 400 throw nestjs HttpException - // the error response will be generated by nestjs - // caller can use try/catch to deal with this manually also - if (response.status >= 400) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - throw new HttpException(response.body as Record, response.status) - } - return response.body - } -} diff --git a/packages/server/src/nestjs/index.ts b/packages/server/src/nestjs/index.ts deleted file mode 100644 index f02976629..000000000 --- a/packages/server/src/nestjs/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './zenstack.module'; -export * from './api-handler.service'; -export * from './zenstack.constants'; diff --git a/packages/server/src/nestjs/interfaces/api-handler-options.interface.ts b/packages/server/src/nestjs/interfaces/api-handler-options.interface.ts deleted file mode 100644 index fe497afe9..000000000 --- a/packages/server/src/nestjs/interfaces/api-handler-options.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { AdapterBaseOptions } from "../../types"; - -export interface ApiHandlerOptions extends AdapterBaseOptions { - /** - * The base URL for the API handler. This is used to determine the base path for the API requests. - * If you are using the ApiHandlerService in a route with a prefix, you should set this to the prefix. - * - * e.g. - * without baseUrl(API handler default route): - * - RPC API handler: [model]/findMany - * - RESTful API handler: /:type - * - * with baseUrl(/api/crud): - * - RPC API handler: /api/crud/[model]/findMany - * - RESTful API handler: /api/crud/:type - */ - baseUrl?: string; -} diff --git a/packages/server/src/nestjs/interfaces/index.ts b/packages/server/src/nestjs/interfaces/index.ts deleted file mode 100644 index ea713be4e..000000000 --- a/packages/server/src/nestjs/interfaces/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './zenstack-module-options.interface' -export * from './api-handler-options.interface' diff --git a/packages/server/src/nestjs/interfaces/zenstack-module-options.interface.ts b/packages/server/src/nestjs/interfaces/zenstack-module-options.interface.ts deleted file mode 100644 index e2b45d6ea..000000000 --- a/packages/server/src/nestjs/interfaces/zenstack-module-options.interface.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { FactoryProvider, ModuleMetadata, Provider } from "@nestjs/common"; - -/** - * ZenStack module options. - */ -export interface ZenStackModuleOptions { - /** - * A callback for getting an enhanced `PrismaClient`. - */ - getEnhancedPrisma: (model?: string | symbol) => unknown; -} - -/** - * ZenStack module async registration options. - */ -export interface ZenStackModuleAsyncOptions extends Pick { - /** - * Whether the module is global-scoped. - */ - global?: boolean; - - /** - * The token to export the enhanced Prisma service. Default is {@link ENHANCED_PRISMA}. - */ - exportToken?: string; - - /** - * The factory function to create the enhancement options. - */ - useFactory: (...args: unknown[]) => Promise | ZenStackModuleOptions; - - /** - * The dependencies to inject into the factory function. - */ - inject?: FactoryProvider['inject']; - - /** - * Extra providers to facilitate dependency injection. - */ - extraProviders?: Provider[]; -} diff --git a/packages/server/src/nestjs/zenstack.constants.ts b/packages/server/src/nestjs/zenstack.constants.ts deleted file mode 100644 index bf082f025..000000000 --- a/packages/server/src/nestjs/zenstack.constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * The default token used to export the enhanced Prisma service. - */ -export const ENHANCED_PRISMA = 'ENHANCED_PRISMA'; diff --git a/packages/server/src/nestjs/zenstack.module.ts b/packages/server/src/nestjs/zenstack.module.ts deleted file mode 100644 index 3fff29e8e..000000000 --- a/packages/server/src/nestjs/zenstack.module.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Module, type DynamicModule } from '@nestjs/common'; -import { ENHANCED_PRISMA } from './zenstack.constants'; -import { ZenStackModuleAsyncOptions } from './interfaces'; - -/** - * The ZenStack module for NestJS. The module exports an enhanced Prisma service, - * by default with token {@link ENHANCED_PRISMA}. - */ -@Module({}) -export class ZenStackModule { - /** - * Registers the ZenStack module with the specified options. - */ - static registerAsync(options: ZenStackModuleAsyncOptions): DynamicModule { - return { - module: ZenStackModule, - global: options?.global, - imports: options.imports, - providers: [ - { - provide: options.exportToken ?? ENHANCED_PRISMA, - useFactory: async ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...args: unknown[] - ) => { - const { getEnhancedPrisma } = await options.useFactory(...args); - if (!getEnhancedPrisma) { - throw new Error('`getEnhancedPrisma` must be provided in the options'); - } - - // create a proxy to intercept all calls to the Prisma service and forward - // to the enhanced version - - return new Proxy( - {}, - { - get(_target, prop) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const enhancedPrisma: any = getEnhancedPrisma(prop); - if (!enhancedPrisma) { - throw new Error('`getEnhancedPrisma` must return a valid Prisma client'); - } - return enhancedPrisma[prop]; - }, - } - ); - }, - inject: options.inject, - }, - ...(options.extraProviders ?? []), - ], - exports: [options.exportToken ?? ENHANCED_PRISMA], - }; - } -} diff --git a/packages/server/src/next/app-route-handler.ts b/packages/server/src/next/app-route-handler.ts deleted file mode 100644 index d0894a816..000000000 --- a/packages/server/src/next/app-route-handler.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -import { DbClientContract } from '@zenstackhq/runtime'; -import { NextRequest, NextResponse } from 'next/server'; -import { AppRouteRequestHandlerOptions } from '.'; -import { RPCApiHandler } from '../api'; -import { loadAssets } from '../shared'; - -type Context = { params: Promise<{ path: string[] }> }; - -/** - * Creates a Next.js "app dir" API route request handler which encapsulates Prisma CRUD operations. - * - * @remarks Since Next.js 15, `context.params` is asynchronous and must be awaited. - * @param options Options for initialization - * @returns An API route request handler - */ -export default function factory( - options: AppRouteRequestHandlerOptions -): (req: NextRequest, context: Context) => Promise { - const { modelMeta, zodSchemas } = loadAssets(options); - - const requestHandler = options.handler || RPCApiHandler(); - - return async (req: NextRequest, context: Context) => { - const prisma = (await options.getPrisma(req)) as DbClientContract; - if (!prisma) { - return NextResponse.json({ message: 'unable to get prisma from request context' }, { status: 500 }); - } - - let params: Awaited; - const url = new URL(req.url); - const query = Object.fromEntries(url.searchParams); - - try { - params = await context.params; - } catch { - return NextResponse.json({ message: 'Failed to resolve request parameters' }, { status: 500 }); - } - - if (!params.path) { - return NextResponse.json( - { message: 'missing path parameter' }, - { - status: 400, - } - ); - } - const path = params.path.join('/'); - - let requestBody: unknown; - if (req.body) { - try { - requestBody = await req.json(); - } catch { - // noop - } - } - - try { - const r = await requestHandler({ - method: req.method!, - path, - query, - requestBody, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - return NextResponse.json(r.body, { status: r.status }); - } catch (err) { - return NextResponse.json({ message: `An unhandled error occurred: ${err}` }, { status: 500 }); - } - }; -} diff --git a/packages/server/src/next/index.ts b/packages/server/src/next/index.ts deleted file mode 100644 index 33efdf18f..000000000 --- a/packages/server/src/next/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import type { NextRequest } from 'next/server'; -import type { AdapterBaseOptions } from '../types'; -import { default as AppRouteHandler } from './app-route-handler'; -import { default as PagesRouteHandler } from './pages-route-handler'; - -/** - * Options for initializing a Next.js API endpoint request handler. - */ -export interface PagesRouteRequestHandlerOptions extends AdapterBaseOptions { - /** - * Callback method for getting a Prisma instance for the given request/response pair. - */ - getPrisma: (req: NextApiRequest, res: NextApiResponse) => Promise | unknown; - - /** - * Use Next.js 13 app dir or not - */ - useAppDir?: false | undefined; -} - -/** - * Options for initializing a Next.js 13 app dir API route handler. - */ -export interface AppRouteRequestHandlerOptions extends AdapterBaseOptions { - /** - * Callback method for getting a Prisma instance for the given request. - */ - getPrisma: (req: NextRequest) => Promise | unknown; - - /** - * Use Next.js 13 app dir or not - */ - useAppDir: true; -} - -/** - * Creates a Next.js API route handler. - * @see https://zenstack.dev/docs/reference/server-adapters/next - */ -export function NextRequestHandler(options: PagesRouteRequestHandlerOptions): ReturnType; -export function NextRequestHandler(options: AppRouteRequestHandlerOptions): ReturnType; -export function NextRequestHandler(options: PagesRouteRequestHandlerOptions | AppRouteRequestHandlerOptions) { - if (options.useAppDir === true) { - return AppRouteHandler(options); - } else { - return PagesRouteHandler(options); - } -} - -// for backward compatibility -export { PagesRouteRequestHandlerOptions as RequestHandlerOptions }; diff --git a/packages/server/src/next/pages-route-handler.ts b/packages/server/src/next/pages-route-handler.ts deleted file mode 100644 index dd25b0c6c..000000000 --- a/packages/server/src/next/pages-route-handler.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -import { DbClientContract } from '@zenstackhq/runtime'; -import { NextApiRequest, NextApiResponse } from 'next'; -import { PagesRouteRequestHandlerOptions } from '.'; -import { RPCApiHandler } from '../api'; -import { loadAssets } from '../shared'; - -/** - * Creates a Next.js API endpoint (traditional "pages" route) request handler which encapsulates Prisma CRUD operations. - * - * @param options Options for initialization - * @returns An API endpoint request handler - */ -export default function factory( - options: PagesRouteRequestHandlerOptions -): (req: NextApiRequest, res: NextApiResponse) => Promise { - const { modelMeta, zodSchemas } = loadAssets(options); - - const requestHandler = options.handler || RPCApiHandler(); - - return async (req: NextApiRequest, res: NextApiResponse) => { - const prisma = (await options.getPrisma(req, res)) as DbClientContract; - if (!prisma) { - res.status(500).json({ message: 'unable to get prisma from request context' }); - return; - } - - if (!req.query.path) { - res.status(400).json({ message: 'missing path parameter' }); - return; - } - const path = (req.query.path as string[]).join('/'); - - try { - const r = await requestHandler({ - method: req.method!, - path, - query: req.query as Record, - requestBody: req.body, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - res.status(r.status).send(r.body); - } catch (err) { - res.status(500).send({ message: `An unhandled error occurred: ${err}` }); - } - }; -} diff --git a/packages/server/src/nuxt/handler.ts b/packages/server/src/nuxt/handler.ts deleted file mode 100644 index c9f5042ff..000000000 --- a/packages/server/src/nuxt/handler.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { DbClientContract } from '@zenstackhq/runtime'; -import { - EventHandlerRequest, - H3Event, - defineEventHandler, - getQuery, - getRouterParams, - readBody, - setResponseStatus, -} from 'h3'; -import RPCApiHandler from '../api/rpc'; -import { loadAssets } from '../shared'; -import { AdapterBaseOptions } from '../types'; - -/** - * Nuxt request handler options - */ -export interface HandlerOptions extends AdapterBaseOptions { - /** - * Callback for getting a PrismaClient for the given request - */ - getPrisma: (event: H3Event) => unknown | Promise; -} - -export function createEventHandler(options: HandlerOptions) { - return defineEventHandler(async (event) => { - const { modelMeta, zodSchemas } = loadAssets(options); - const requestHandler = options.handler ?? RPCApiHandler(); - - const prisma = (await options.getPrisma(event)) as DbClientContract; - if (!prisma) { - setResponseStatus(event, 500); - return { message: 'unable to get prisma from request context' }; - } - - const routerParam = getRouterParams(event); - const query = await getQuery(event); - - let reqBody: unknown; - if (event.method === 'POST' || event.method === 'PUT' || event.method === 'PATCH') { - reqBody = await readBody(event); - } - - try { - const { status, body } = await requestHandler({ - method: event.method, - path: routerParam._, - query: query as Record, - requestBody: reqBody, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - - setResponseStatus(event, status); - return body; - } catch (err) { - setResponseStatus(event, 500); - return { message: `An unhandled error occurred: ${err}` }; - } - }); -} diff --git a/packages/server/src/nuxt/index.ts b/packages/server/src/nuxt/index.ts deleted file mode 100644 index 68ae53f6c..000000000 --- a/packages/server/src/nuxt/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './handler'; diff --git a/packages/server/src/shared.ts b/packages/server/src/shared.ts deleted file mode 100644 index 27a07ddd9..000000000 --- a/packages/server/src/shared.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -import type { ModelMeta, ZodSchemas } from '@zenstackhq/runtime'; -import { AdapterBaseOptions } from './types'; - -export function loadAssets(options: AdapterBaseOptions) { - // model metadata - const modelMeta = options.modelMeta ?? getDefaultModelMeta(); - - // zod schemas - let zodSchemas: ZodSchemas | undefined; - if (typeof options.zodSchemas === 'object') { - zodSchemas = options.zodSchemas; - } else if (options.zodSchemas === true) { - zodSchemas = getDefaultZodSchemas(); - if (!zodSchemas) { - throw new Error('Unable to load zod schemas from default location'); - } - } - - return { modelMeta, zodSchemas }; -} - -/** - * Load model metadata. - * - * @param loadPath The path to load model metadata from. If not provided, - * will use default load path. - */ -export function getDefaultModelMeta(): ModelMeta { - try { - return require('@zenstackhq/runtime/model-meta').default; - } catch { - throw new Error('Model meta cannot be loaded. Please make sure "zenstack generate" has been run.'); - } -} - -/** - * Load zod schemas. - * - * @param loadPath The path to load zod schemas from. If not provided, - * will use default load path. - */ -export function getDefaultZodSchemas(): ZodSchemas | undefined { - try { - return require('@zenstackhq/runtime/zod'); - } catch { - return undefined; - } -} diff --git a/packages/server/src/sveltekit/handler.ts b/packages/server/src/sveltekit/handler.ts deleted file mode 100644 index 0721c1f86..000000000 --- a/packages/server/src/sveltekit/handler.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { Handle, RequestEvent } from '@sveltejs/kit'; -import { DbClientContract } from '@zenstackhq/runtime'; -import RPCApiHandler from '../api/rpc'; -import { logInfo } from '../api/utils'; -import { loadAssets } from '../shared'; -import { AdapterBaseOptions } from '../types'; - -/** - * SvelteKit request handler options - */ -export interface HandlerOptions extends AdapterBaseOptions { - /** - * Url prefix, e.g.: /api - */ - prefix: string; - - /** - * Callback for getting a PrismaClient for the given request - */ - getPrisma: (event: RequestEvent) => unknown | Promise; -} - -/** - * SvelteKit server hooks handler for handling CRUD requests. - */ -export default function createHandler(options: HandlerOptions): Handle { - logInfo(options.logger, `ZenStackHandler installing routes at prefix: ${options.prefix}`); - - const { modelMeta, zodSchemas } = loadAssets(options); - - const requestHandler = options.handler ?? RPCApiHandler(); - - return async ({ event, resolve }) => { - if (event.url.pathname.startsWith(options.prefix)) { - const prisma = (await options.getPrisma(event)) as DbClientContract; - if (!prisma) { - return new Response(JSON.stringify({ message: 'unable to get prisma from request context' }), { - status: 400, - headers: { - 'content-type': 'application/json', - }, - }); - } - - const query = Object.fromEntries(event.url.searchParams); - let requestBody: unknown; - if (event.request.body) { - const text = await event.request.text(); - if (text) { - requestBody = JSON.parse(text); - } - } - - const path = event.url.pathname.substring(options.prefix.length); - - try { - const r = await requestHandler({ - method: event.request.method, - path, - query, - requestBody, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - - return new Response(JSON.stringify(r.body), { - status: r.status, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (err) { - return new Response(JSON.stringify({ message: `An unhandled error occurred: ${err}` }), { - status: 500, - headers: { - 'content-type': 'application/json', - }, - }); - } - } - - return resolve(event); - }; -} - -export { createHandler as SvelteKitHandler }; diff --git a/packages/server/src/sveltekit/index.ts b/packages/server/src/sveltekit/index.ts deleted file mode 100644 index 7f040d76f..000000000 --- a/packages/server/src/sveltekit/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SvelteKitHandler } from './handler'; -export * from './handler'; diff --git a/packages/server/src/tanstack-start/handler.ts b/packages/server/src/tanstack-start/handler.ts deleted file mode 100644 index 63a98e10b..000000000 --- a/packages/server/src/tanstack-start/handler.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -import { DbClientContract } from '@zenstackhq/runtime'; -import { TanStackStartOptions } from '.'; -import { RPCApiHandler } from '../api'; -import { loadAssets } from '../shared'; - -/** - * Creates a TanStack Start server route handler which encapsulates Prisma CRUD operations. - * - * @param options Options for initialization - * @returns A TanStack Start server route handler - */ -export default function factory( - options: TanStackStartOptions -): ({ request, params }: { request: Request; params: Record }) => Promise { - const { modelMeta, zodSchemas } = loadAssets(options); - - const requestHandler = options.handler || RPCApiHandler(); - - return async ({ request, params }: { request: Request; params: Record }) => { - const prisma = (await options.getPrisma(request, params)) as DbClientContract; - if (!prisma) { - return new Response(JSON.stringify({ message: 'unable to get prisma from request context' }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - }, - }); - } - - const url = new URL(request.url); - const query = Object.fromEntries(url.searchParams); - - // Extract path from params._splat for catch-all routes - const path = params._splat; - - if (!path) { - return new Response(JSON.stringify({ message: 'missing path parameter' }), { - status: 400, - headers: { - 'Content-Type': 'application/json', - }, - }); - } - - let requestBody: unknown; - if (request.body) { - try { - requestBody = await request.json(); - } catch { - // noop - } - } - - try { - const r = await requestHandler({ - method: request.method!, - path, - query, - requestBody, - prisma, - modelMeta, - zodSchemas, - logger: options.logger, - }); - return new Response(JSON.stringify(r.body), { - status: r.status, - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (err) { - return new Response(JSON.stringify({ message: `An unhandled error occurred: ${err}` }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - }, - }); - } - }; -} - diff --git a/packages/server/src/tanstack-start/index.ts b/packages/server/src/tanstack-start/index.ts deleted file mode 100644 index ecb62543f..000000000 --- a/packages/server/src/tanstack-start/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { AdapterBaseOptions } from '../types'; -import { default as Handler } from './handler'; - -/** - * Options for initializing a TanStack Start server route handler. - */ -export interface TanStackStartOptions extends AdapterBaseOptions { - /** - * Callback method for getting a Prisma instance for the given request and params. - */ - getPrisma: (request: Request, params: Record) => Promise | unknown; -} - -/** - * Creates a TanStack Start server route handler. - * @see https://zenstack.dev/docs/reference/server-adapters/tanstack-start - */ -export function TanStackStartHandler(options: TanStackStartOptions): ReturnType { - return Handler(options); -} - -export default TanStackStartHandler; - diff --git a/packages/server/tests/adapter/elysia.test.ts b/packages/server/tests/adapter/elysia.test.ts deleted file mode 100644 index d004e5331..000000000 --- a/packages/server/tests/adapter/elysia.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// -import { loadSchema } from '@zenstackhq/testtools'; -import 'isomorphic-fetch'; -import path from 'path'; -import superjson from 'superjson'; -import Rest from '../../src/api/rest'; -import { createElysiaHandler } from '../../src/elysia'; -import { makeUrl, schema } from '../utils'; -import { Elysia } from 'elysia'; - -describe('Elysia adapter tests - rpc handler', () => { - it('run hooks regular json', async () => { - const { prisma, zodSchemas } = await loadSchema(schema); - - const handler = await createElysiaApp( - createElysiaHandler({ getPrisma: () => prisma, zodSchemas, basePath: '/api' }) - ); - - let r = await handler(makeRequest('GET', makeUrl('/api/post/findMany', { where: { id: { equals: '1' } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler( - makeRequest('POST', '/api/user/create', { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }) - ); - expect(r.status).toBe(201); - expect((await unmarshal(r)).data).toMatchObject({ - email: 'user1@abc.com', - posts: expect.arrayContaining([ - expect.objectContaining({ title: 'post1' }), - expect.objectContaining({ title: 'post2' }), - ]), - }); - - r = await handler(makeRequest('GET', makeUrl('/api/post/findMany'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(2); - - r = await handler(makeRequest('GET', makeUrl('/api/post/findMany', { where: { viewCount: { gt: 1 } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(1); - - r = await handler( - makeRequest('PUT', '/api/user/update', { where: { id: 'user1' }, data: { email: 'user1@def.com' } }) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.email).toBe('user1@def.com'); - - r = await handler(makeRequest('GET', makeUrl('/api/post/count', { where: { viewCount: { gt: 1 } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toBe(1); - - r = await handler(makeRequest('GET', makeUrl('/api/post/aggregate', { _sum: { viewCount: true } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data._sum.viewCount).toBe(3); - - r = await handler( - makeRequest('GET', makeUrl('/api/post/groupBy', { by: ['published'], _sum: { viewCount: true } })) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toEqual( - expect.arrayContaining([ - expect.objectContaining({ published: true, _sum: { viewCount: 1 } }), - expect.objectContaining({ published: false, _sum: { viewCount: 2 } }), - ]) - ); - - r = await handler(makeRequest('DELETE', makeUrl('/api/user/deleteMany', { where: { id: 'user1' } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.count).toBe(1); - }); - - // TODO: failing in CI - // eslint-disable-next-line jest/no-disabled-tests - it.skip('custom load path', async () => { - const { prisma, projectDir } = await loadSchema(schema, { output: './zen' }); - - const handler = await createElysiaApp( - createElysiaHandler({ - getPrisma: () => prisma, - basePath: '/api', - modelMeta: require(path.join(projectDir, './zen/model-meta')).default, - zodSchemas: require(path.join(projectDir, './zen/zod')), - }) - ); - - const r = await handler( - makeRequest('POST', '/api/user/create', { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }) - ); - expect(r.status).toBe(201); - }); -}); - -describe('Elysia adapter tests - rest handler', () => { - it('run hooks', async () => { - const { prisma, modelMeta, zodSchemas } = await loadSchema(schema); - - const handler = await createElysiaApp( - createElysiaHandler({ - getPrisma: () => prisma, - basePath: '/api', - handler: Rest({ endpoint: 'http://localhost/api' }), - modelMeta, - zodSchemas, - }) - ); - - let r = await handler(makeRequest('GET', makeUrl('/api/post/1'))); - expect(r.status).toBe(404); - - r = await handler( - makeRequest('POST', '/api/user', { - data: { - type: 'user', - attributes: { id: 'user1', email: 'user1@abc.com' }, - }, - }) - ); - expect(r.status).toBe(201); - expect(await unmarshal(r)).toMatchObject({ - data: { - id: 'user1', - attributes: { - email: 'user1@abc.com', - }, - }, - }); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user1'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(1); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user2'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user1&filter[email]=xyz'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler( - makeRequest('PUT', makeUrl('/api/user/user1'), { - data: { type: 'user', attributes: { email: 'user1@def.com' } }, - }) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.attributes.email).toBe('user1@def.com'); - - r = await handler(makeRequest('DELETE', makeUrl('/api/user/user1'))); - expect(r.status).toBe(200); - expect(await prisma.user.findMany()).toHaveLength(0); - }); -}); - -function makeRequest(method: string, path: string, body?: any) { - if (body) { - return new Request(`http://localhost${path}`, { - method, - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }); - } else { - return new Request(`http://localhost${path}`, { method }); - } -} - -async function unmarshal(r: Response, useSuperJson = false) { - const text = await r.text(); - return (useSuperJson ? superjson.parse(text) : JSON.parse(text)) as any; -} - -async function createElysiaApp(middleware: (app: Elysia) => Promise) { - const app = new Elysia(); - await middleware(app); - return app.handle; -} diff --git a/packages/server/tests/adapter/express.test.ts b/packages/server/tests/adapter/express.test.ts deleted file mode 100644 index 85ccc8a21..000000000 --- a/packages/server/tests/adapter/express.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import { loadSchema } from '@zenstackhq/testtools'; -import bodyParser from 'body-parser'; -import express from 'express'; -import request from 'supertest'; -import RESTAPIHandler from '../../src/api/rest'; -import { ZenStackMiddleware } from '../../src/express'; -import { makeUrl, schema } from '../utils'; -import path from 'path'; - -describe('Express adapter tests - rpc handler', () => { - it('run plugin regular json', async () => { - const { prisma, zodSchemas } = await loadSchema(schema); - - const app = express(); - app.use(bodyParser.json()); - app.use('/api', ZenStackMiddleware({ getPrisma: () => prisma, zodSchemas })); - - let r = await request(app).get(makeUrl('/api/post/findMany', { where: { id: { equals: '1' } } })); - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(0); - - r = await request(app) - .post('/api/user/create') - .send({ - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }); - - expect(r.status).toBe(201); - const data = r.body.data; - expect(data).toEqual( - expect.objectContaining({ - email: 'user1@abc.com', - posts: expect.arrayContaining([ - expect.objectContaining({ title: 'post1' }), - expect.objectContaining({ title: 'post2' }), - ]), - }) - ); - - r = await request(app).get(makeUrl('/api/post/findMany')); - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(2); - - r = await request(app).get(makeUrl('/api/post/findMany', { where: { viewCount: { gt: 1 } } })); - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(1); - - r = await request(app) - .put('/api/user/update') - .send({ where: { id: 'user1' }, data: { email: 'user1@def.com' } }); - expect(r.status).toBe(200); - expect(r.body.data.email).toBe('user1@def.com'); - - r = await request(app).get(makeUrl('/api/post/count', { where: { viewCount: { gt: 1 } } })); - expect(r.status).toBe(200); - expect(r.body.data).toBe(1); - - r = await request(app).get(makeUrl('/api/post/aggregate', { _sum: { viewCount: true } })); - expect(r.status).toBe(200); - expect(r.body.data._sum.viewCount).toBe(3); - - r = await request(app).get(makeUrl('/api/post/groupBy', { by: ['published'], _sum: { viewCount: true } })); - expect(r.status).toBe(200); - expect(r.body.data).toEqual( - expect.arrayContaining([ - expect.objectContaining({ published: true, _sum: { viewCount: 1 } }), - expect.objectContaining({ published: false, _sum: { viewCount: 2 } }), - ]) - ); - - r = await request(app).delete(makeUrl('/api/user/deleteMany', { where: { id: 'user1' } })); - expect(r.status).toBe(200); - expect(r.body.data.count).toBe(1); - }); - - it('custom load path', async () => { - const { prisma, projectDir } = await loadSchema(schema, { output: './zen' }); - - const app = express(); - app.use(bodyParser.json()); - app.use( - '/api', - ZenStackMiddleware({ - getPrisma: () => prisma, - modelMeta: require(path.join(projectDir, './zen/model-meta')).default, - zodSchemas: require(path.join(projectDir, './zen/zod')), - }) - ); - - const r = await request(app) - .post('/api/user/create') - .send({ - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }); - - expect(r.status).toBe(201); - }); - - it('invalid path or args', async () => { - const { prisma, zodSchemas } = await loadSchema(schema); - - const app = express(); - app.use(bodyParser.json()); - app.use('/api', ZenStackMiddleware({ getPrisma: () => prisma, zodSchemas })); - - let r = await request(app).get('/api/post/'); - expect(r.status).toBe(400); - - r = await request(app).get('/api/post/findMany/abc'); - expect(r.status).toBe(400); - - r = await request(app).get('/api/post/findMany?q=abc'); - expect(r.status).toBe(400); - }); -}); - -describe('Express adapter tests - rest handler', () => { - it('run middleware', async () => { - const { prisma, zodSchemas, modelMeta } = await loadSchema(schema); - - const app = express(); - app.use(bodyParser.json()); - app.use( - '/api', - ZenStackMiddleware({ - getPrisma: () => prisma, - modelMeta, - zodSchemas, - handler: RESTAPIHandler({ endpoint: 'http://localhost/api' }), - }) - ); - - let r = await request(app).get(makeUrl('/api/post/1')); - expect(r.status).toBe(404); - - r = await request(app) - .post('/api/user') - .send({ - data: { - type: 'user', - attributes: { - id: 'user1', - email: 'user1@abc.com', - }, - }, - }); - expect(r.status).toBe(201); - expect(r.body).toMatchObject({ - jsonapi: { version: '1.1' }, - data: { type: 'user', id: 'user1', attributes: { email: 'user1@abc.com' } }, - }); - - r = await request(app).get('/api/user?filter[id]=user1'); - expect(r.body.data).toHaveLength(1); - - r = await request(app).get('/api/user?filter[id]=user2'); - expect(r.body.data).toHaveLength(0); - - r = await request(app).get('/api/user?filter[id]=user1&filter[email]=xyz'); - expect(r.body.data).toHaveLength(0); - - r = await request(app) - .put('/api/user/user1') - .send({ data: { type: 'user', attributes: { email: 'user1@def.com' } } }); - expect(r.status).toBe(200); - expect(r.body.data.attributes.email).toBe('user1@def.com'); - - r = await request(app).delete(makeUrl('/api/user/user1')); - expect(r.status).toBe(200); - expect(await prisma.user.findMany()).toHaveLength(0); - }); -}); - -describe('Express adapter tests - rest handler with custom middleware', () => { - it('run middleware', async () => { - const { prisma, zodSchemas, modelMeta } = await loadSchema(schema); - - const app = express(); - app.use(bodyParser.json()); - app.use( - '/api', - ZenStackMiddleware({ - getPrisma: () => prisma, - modelMeta, - zodSchemas, - handler: RESTAPIHandler({ endpoint: 'http://localhost/api' }), - sendResponse: false, - }) - ); - - app.use((req, res) => { - res.status(res.locals.status).json({ message: res.locals.body }); - }); - - const r = await request(app).get(makeUrl('/api/post/1')); - expect(r.status).toBe(404); - expect(r.body.message).toHaveProperty('errors'); - }); -}); diff --git a/packages/server/tests/adapter/fastify.test.ts b/packages/server/tests/adapter/fastify.test.ts deleted file mode 100644 index ed4da3c72..000000000 --- a/packages/server/tests/adapter/fastify.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import { loadSchema } from '@zenstackhq/testtools'; -import fastify from 'fastify'; -import path from 'path'; -import Rest from '../../src/api/rest'; -import RPC from '../../src/api/rpc'; -import { ZenStackFastifyPlugin } from '../../src/fastify'; -import { makeUrl, schema } from '../utils'; - -describe('Fastify adapter tests - rpc handler', () => { - it('run plugin regular json', async () => { - const { prisma, zodSchemas } = await loadSchema(schema); - - const app = fastify(); - app.register(ZenStackFastifyPlugin, { - prefix: '/api', - getPrisma: () => prisma, - zodSchemas, - handler: RPC(), - }); - - let r = await app.inject({ - method: 'GET', - url: makeUrl('/api/post/findMany', { where: { id: { equals: '1' } } }), - }); - expect(r.statusCode).toBe(200); - expect(r.json().data).toHaveLength(0); - - r = await app.inject({ - method: 'POST', - url: '/api/user/create', - payload: { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }, - }); - expect(r.statusCode).toBe(201); - const data = r.json().data; - expect(data).toEqual( - expect.objectContaining({ - email: 'user1@abc.com', - posts: expect.arrayContaining([ - expect.objectContaining({ title: 'post1' }), - expect.objectContaining({ title: 'post2' }), - ]), - }) - ); - - r = await app.inject({ - method: 'GET', - url: makeUrl('/api/post/findMany'), - }); - expect(r.statusCode).toBe(200); - expect(r.json().data).toHaveLength(2); - - r = await app.inject({ - method: 'GET', - url: makeUrl('/api/post/findMany', { where: { viewCount: { gt: 1 } } }), - }); - expect(r.statusCode).toBe(200); - expect(r.json().data).toHaveLength(1); - - r = await app.inject({ - method: 'PUT', - url: '/api/user/update', - payload: { where: { id: 'user1' }, data: { email: 'user1@def.com' } }, - }); - expect(r.statusCode).toBe(200); - expect(r.json().data.email).toBe('user1@def.com'); - - r = await app.inject({ - method: 'GET', - url: makeUrl('/api/post/count', { where: { viewCount: { gt: 1 } } }), - }); - expect(r.statusCode).toBe(200); - expect(r.json().data).toBe(1); - - r = await app.inject({ - method: 'GET', - url: makeUrl('/api/post/aggregate', { _sum: { viewCount: true } }), - }); - expect(r.statusCode).toBe(200); - expect(r.json().data._sum.viewCount).toBe(3); - - r = await app.inject({ - method: 'GET', - url: makeUrl('/api/post/groupBy', { by: ['published'], _sum: { viewCount: true } }), - }); - expect(r.statusCode).toBe(200); - expect(r.json().data).toEqual( - expect.arrayContaining([ - expect.objectContaining({ published: true, _sum: { viewCount: 1 } }), - expect.objectContaining({ published: false, _sum: { viewCount: 2 } }), - ]) - ); - - r = await app.inject({ - method: 'DELETE', - url: makeUrl('/api/user/deleteMany', { where: { id: 'user1' } }), - }); - expect(r.statusCode).toBe(200); - expect(r.json().data.count).toBe(1); - }); - - it('custom load path', async () => { - const { prisma, projectDir } = await loadSchema(schema, { output: './zen' }); - - const app = fastify(); - app.register(ZenStackFastifyPlugin, { - prefix: '/api', - getPrisma: () => prisma, - modelMeta: require(path.join(projectDir, './zen/model-meta')).default, - zodSchemas: require(path.join(projectDir, './zen/zod')), - handler: RPC(), - }); - - const r = await app.inject({ - method: 'POST', - url: '/api/user/create', - payload: { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }, - }); - expect(r.statusCode).toBe(201); - }); - - it('invalid path or args', async () => { - const { prisma, zodSchemas } = await loadSchema(schema); - - const app = fastify(); - app.register(ZenStackFastifyPlugin, { - prefix: '/api', - getPrisma: () => prisma, - zodSchemas, - handler: RPC(), - }); - - let r = await app.inject({ - method: 'GET', - url: '/api/post/', - }); - expect(r.statusCode).toBe(400); - - r = await app.inject({ - method: 'GET', - url: '/api/post/findMany/abc', - }); - expect(r.statusCode).toBe(400); - - r = await app.inject({ - method: 'GET', - url: '/api/post/findMany?q=abc', - }); - expect(r.statusCode).toBe(400); - }); -}); - -describe('Fastify adapter tests - rest handler', () => { - it('run plugin regular json', async () => { - const { prisma, zodSchemas, modelMeta } = await loadSchema(schema); - - const app = fastify(); - app.register(ZenStackFastifyPlugin, { - prefix: '/api', - getPrisma: () => prisma, - modelMeta, - zodSchemas, - handler: Rest({ endpoint: 'http://localhost/api' }), - }); - - let r = await app.inject({ - method: 'GET', - url: '/api/post/1', - }); - expect(r.statusCode).toBe(404); - - r = await app.inject({ - method: 'POST', - url: '/api/user', - payload: { - data: { - type: 'user', - attributes: { - id: 'user1', - email: 'user1@abc.com', - }, - }, - }, - }); - expect(r.statusCode).toBe(201); - expect(r.json()).toMatchObject({ - jsonapi: { version: '1.1' }, - data: { type: 'user', id: 'user1', attributes: { email: 'user1@abc.com' } }, - }); - - r = await app.inject({ method: 'GET', url: '/api/user?filter[id]=user1' }); - expect(r.json().data).toHaveLength(1); - - r = await app.inject({ method: 'GET', url: '/api/user?filter[id]=user2' }); - expect(r.json().data).toHaveLength(0); - - r = await app.inject({ method: 'GET', url: '/api/user?filter[id]=user1&filter[email]=xyz' }); - expect(r.json().data).toHaveLength(0); - - r = await app.inject({ - method: 'PUT', - url: '/api/user/user1', - payload: { data: { type: 'user', attributes: { email: 'user1@def.com' } } }, - }); - expect(r.statusCode).toBe(200); - expect(r.json().data.attributes.email).toBe('user1@def.com'); - - r = await app.inject({ method: 'DELETE', url: '/api/user/user1' }); - expect(r.statusCode).toBe(200); - expect(await prisma.user.findMany()).toHaveLength(0); - }); -}); diff --git a/packages/server/tests/adapter/hono.test.ts b/packages/server/tests/adapter/hono.test.ts deleted file mode 100644 index eb75310be..000000000 --- a/packages/server/tests/adapter/hono.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// -import { loadSchema } from '@zenstackhq/testtools'; -import 'isomorphic-fetch'; -import path from 'path'; -import superjson from 'superjson'; -import Rest from '../../src/api/rest'; -import { createHonoHandler } from '../../src/hono'; -import { makeUrl, schema } from '../utils'; -import { Hono, MiddlewareHandler } from 'hono'; - -describe('Hono adapter tests - rpc handler', () => { - it('run hooks regular json', async () => { - const { prisma, zodSchemas } = await loadSchema(schema); - - const handler = await createHonoApp(createHonoHandler({ getPrisma: () => prisma, zodSchemas })); - - let r = await handler(makeRequest('GET', makeUrl('/api/post/findMany', { where: { id: { equals: '1' } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler( - makeRequest('POST', '/api/user/create', { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }) - ); - expect(r.status).toBe(201); - expect((await unmarshal(r)).data).toMatchObject({ - email: 'user1@abc.com', - posts: expect.arrayContaining([ - expect.objectContaining({ title: 'post1' }), - expect.objectContaining({ title: 'post2' }), - ]), - }); - - r = await handler(makeRequest('GET', makeUrl('/api/post/findMany'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(2); - - r = await handler(makeRequest('GET', makeUrl('/api/post/findMany', { where: { viewCount: { gt: 1 } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(1); - - r = await handler( - makeRequest('PUT', '/api/user/update', { where: { id: 'user1' }, data: { email: 'user1@def.com' } }) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.email).toBe('user1@def.com'); - - r = await handler(makeRequest('GET', makeUrl('/api/post/count', { where: { viewCount: { gt: 1 } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toBe(1); - - r = await handler(makeRequest('GET', makeUrl('/api/post/aggregate', { _sum: { viewCount: true } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data._sum.viewCount).toBe(3); - - r = await handler( - makeRequest('GET', makeUrl('/api/post/groupBy', { by: ['published'], _sum: { viewCount: true } })) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toEqual( - expect.arrayContaining([ - expect.objectContaining({ published: true, _sum: { viewCount: 1 } }), - expect.objectContaining({ published: false, _sum: { viewCount: 2 } }), - ]) - ); - - r = await handler(makeRequest('DELETE', makeUrl('/api/user/deleteMany', { where: { id: 'user1' } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.count).toBe(1); - }); - - // TODO: investigate failure in CI - // eslint-disable-next-line jest/no-disabled-tests - it.skip('custom load path', async () => { - const { prisma, projectDir } = await loadSchema(schema, { output: './zen' }); - - const handler = await createHonoApp( - createHonoHandler({ - getPrisma: () => prisma, - modelMeta: require(path.join(projectDir, './zen/model-meta')).default, - zodSchemas: require(path.join(projectDir, './zen/zod')), - }) - ); - - const r = await handler( - makeRequest('POST', '/api/user/create', { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }) - ); - expect(r.status).toBe(201); - }); -}); - -describe('Hono adapter tests - rest handler', () => { - it('run hooks', async () => { - const { prisma, modelMeta, zodSchemas } = await loadSchema(schema); - - const handler = await createHonoApp( - createHonoHandler({ - getPrisma: () => prisma, - handler: Rest({ endpoint: 'http://localhost/api' }), - modelMeta, - zodSchemas, - }) - ); - - let r = await handler(makeRequest('GET', makeUrl('/api/post/1'))); - expect(r.status).toBe(404); - - r = await handler( - makeRequest('POST', '/api/user', { - data: { - type: 'user', - attributes: { id: 'user1', email: 'user1@abc.com' }, - }, - }) - ); - expect(r.status).toBe(201); - expect(await unmarshal(r)).toMatchObject({ - data: { - id: 'user1', - attributes: { - email: 'user1@abc.com', - }, - }, - }); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user1'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(1); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user2'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user1&filter[email]=xyz'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler( - makeRequest('PUT', makeUrl('/api/user/user1'), { - data: { type: 'user', attributes: { email: 'user1@def.com' } }, - }) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.attributes.email).toBe('user1@def.com'); - - r = await handler(makeRequest('DELETE', makeUrl('/api/user/user1'))); - expect(r.status).toBe(200); - expect(await prisma.user.findMany()).toHaveLength(0); - }); -}); - -function makeRequest(method: string, path: string, body?: any) { - const payload = body ? JSON.stringify(body) : undefined; - return new Request(`http://localhost${path}`, { method, body: payload }); -} - -async function unmarshal(r: Response, useSuperJson = false) { - const text = await r.text(); - return (useSuperJson ? superjson.parse(text) : JSON.parse(text)) as any; -} - -async function createHonoApp(middleware: MiddlewareHandler) { - const app = new Hono(); - - app.use('/api/*', middleware); - - return app.fetch; -} diff --git a/packages/server/tests/adapter/nestjs.test.ts b/packages/server/tests/adapter/nestjs.test.ts deleted file mode 100644 index c38964403..000000000 --- a/packages/server/tests/adapter/nestjs.test.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { Test } from '@nestjs/testing'; -import { loadSchema } from '@zenstackhq/testtools'; -import { ZenStackModule, ENHANCED_PRISMA, ApiHandlerService } from '../../src/nestjs'; -import { HttpAdapterHost, REQUEST } from '@nestjs/core'; -import RESTApiHandler from '../../src/api/rest'; - -const schema = ` - model User { - id Int @id @default(autoincrement()) - posts Post[] - @@allow('all', true) - } - - model Post { - id Int @id @default(autoincrement()) - title String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId Int - - @@allow('read', published || auth() == author) - } - `; - -describe('NestJS adapter tests', () => { - it('anonymous', async () => { - const { prisma, enhanceRaw } = await loadSchema(schema); - - await prisma.user.create({ - data: { - posts: { - create: [ - { title: 'post1', published: true }, - { title: 'post2', published: false }, - ], - }, - }, - }); - - const moduleRef = await Test.createTestingModule({ - imports: [ - ZenStackModule.registerAsync({ - useFactory: (prismaService) => ({ getEnhancedPrisma: () => enhanceRaw(prismaService) }), - inject: ['PrismaService'], - extraProviders: [ - { - provide: 'PrismaService', - useValue: prisma, - }, - ], - }), - ], - providers: [ - { - provide: 'PostService', - useFactory: (enhancedPrismaService) => ({ - findAll: () => enhancedPrismaService.post.findMany(), - }), - inject: [ENHANCED_PRISMA], - }, - ], - }).compile(); - - const app = moduleRef.createNestApplication(); - await app.init(); - - const postSvc = app.get('PostService'); - await expect(postSvc.findAll()).resolves.toHaveLength(1); - }); - - it('auth user', async () => { - const { prisma, enhanceRaw } = await loadSchema(schema); - - await prisma.user.create({ - data: { - id: 1, - posts: { - create: [ - { title: 'post1', published: true }, - { title: 'post2', published: false }, - ], - }, - }, - }); - - const moduleRef = await Test.createTestingModule({ - imports: [ - ZenStackModule.registerAsync({ - useFactory: (prismaService) => ({ - getEnhancedPrisma: () => enhanceRaw(prismaService, { user: { id: 1 } }), - }), - inject: ['PrismaService'], - extraProviders: [ - { - provide: 'PrismaService', - useValue: prisma, - }, - ], - }), - ], - providers: [ - { - provide: 'PostService', - useFactory: (enhancedPrismaService) => ({ - findAll: () => enhancedPrismaService.post.findMany(), - }), - inject: [ENHANCED_PRISMA], - }, - ], - }).compile(); - - const app = moduleRef.createNestApplication(); - await app.init(); - - const postSvc = app.get('PostService'); - await expect(postSvc.findAll()).resolves.toHaveLength(2); - }); - - it('custom token', async () => { - const { prisma, enhanceRaw } = await loadSchema(schema); - - await prisma.user.create({ - data: { - posts: { - create: [ - { title: 'post1', published: true }, - { title: 'post2', published: false }, - ], - }, - }, - }); - - const moduleRef = await Test.createTestingModule({ - imports: [ - ZenStackModule.registerAsync({ - useFactory: (prismaService) => ({ getEnhancedPrisma: () => enhanceRaw(prismaService) }), - inject: ['PrismaService'], - extraProviders: [ - { - provide: 'PrismaService', - useValue: prisma, - }, - ], - exportToken: 'MyEnhancedPrisma', - }), - ], - providers: [ - { - provide: 'PostService', - useFactory: (enhancedPrismaService) => ({ - findAll: () => enhancedPrismaService.post.findMany(), - }), - inject: ['MyEnhancedPrisma'], - }, - ], - }).compile(); - - const app = moduleRef.createNestApplication(); - await app.init(); - - const postSvc = app.get('PostService'); - await expect(postSvc.findAll()).resolves.toHaveLength(1); - }); - - it('pass property', async () => { - const { prisma, enhanceRaw } = await loadSchema(schema); - - await prisma.user.create({ - data: { - posts: { - create: [ - { title: 'post1', published: true }, - { title: 'post2', published: false }, - ], - }, - }, - }); - - const moduleRef = await Test.createTestingModule({ - imports: [ - ZenStackModule.registerAsync({ - useFactory: (prismaService) => ({ - getEnhancedPrisma: (prop) => { - return prop === 'post' ? prismaService : enhanceRaw(prismaService, { user: { id: 2 } }); - }, - }), - inject: ['PrismaService'], - extraProviders: [ - { - provide: 'PrismaService', - useValue: prisma, - }, - ], - }), - ], - providers: [ - { - provide: 'PostService', - useFactory: (enhancedPrismaService) => ({ - findAll: () => enhancedPrismaService.post.findMany(), - }), - inject: [ENHANCED_PRISMA], - }, - ], - }).compile(); - - const app = moduleRef.createNestApplication(); - await app.init(); - - const postSvc = app.get('PostService'); - await expect(postSvc.findAll()).resolves.toHaveLength(2); - }); -}); - -describe('ApiHandlerService tests', () => { - it('with default option', async () => { - const { prisma, enhanceRaw } = await loadSchema(schema); - - await prisma.user.create({ - data: { - posts: { - create: [ - { title: 'post1', published: true }, - { title: 'post2', published: false }, - ], - }, - }, - }); - - const moduleRef = await Test.createTestingModule({ - imports: [ - ZenStackModule.registerAsync({ - useFactory: (prismaService) => ({ getEnhancedPrisma: () => enhanceRaw(prismaService) }), - inject: ['PrismaService'], - extraProviders: [ - { - provide: 'PrismaService', - useValue: prisma, - }, - ], - }), - ], - providers: [ - { - provide: REQUEST, - useValue: {} - }, - { - provide: HttpAdapterHost, - useValue: { - httpAdapter: { - getRequestHostname: jest.fn().mockReturnValue('localhost'), - getRequestUrl: jest.fn().mockReturnValue('/post/findMany'), - getRequestMethod: jest.fn().mockReturnValue('GET'), - } - } - }, - ApiHandlerService, - ], - }).compile(); - - const service = await moduleRef.resolve(ApiHandlerService); - expect(await service.handleRequest()).toEqual({ - data: [{ - id: 1, - title: 'post1', - published: true, - authorId: 1, - }] - }) - }) - - it('with rest api handler', async () => { - const { prisma, enhanceRaw, modelMeta, zodSchemas } = await loadSchema(schema); - - await prisma.user.create({ - data: { - posts: { - create: [ - { title: 'post1', published: true }, - { title: 'post2', published: false }, - ], - }, - }, - }); - - const moduleRef = await Test.createTestingModule({ - imports: [ - ZenStackModule.registerAsync({ - useFactory: (prismaService) => ({ getEnhancedPrisma: () => enhanceRaw(prismaService) }), - inject: ['PrismaService'], - extraProviders: [ - { - provide: 'PrismaService', - useValue: prisma, - }, - ], - }), - ], - providers: [ - { - provide: REQUEST, - useValue: {} - }, - { - provide: HttpAdapterHost, - useValue: { - httpAdapter: { - getRequestHostname: jest.fn().mockReturnValue('localhost'), - getRequestUrl: jest.fn().mockReturnValue('/post'), - getRequestMethod: jest.fn().mockReturnValue('GET'), - } - } - }, - ApiHandlerService, - ], - }).compile(); - - const service = await moduleRef.resolve(ApiHandlerService); - expect(await service.handleRequest({ - handler: RESTApiHandler({ - endpoint: 'http://localhost', - }), - modelMeta, - zodSchemas, - })).toEqual({ - jsonapi: { - version: "1.1" - }, - data: [{ - type: 'post', - id: 1, - attributes: { - title: 'post1', - published: true, - authorId: 1, - }, - links: { - self: 'http://localhost/post/1', - }, - relationships: { - author: { - data: { - id: 1, - type: 'user', - }, - links: { - related: 'http://localhost/post/1/author', - self: 'http://localhost/post/1/relationships/author', - } - } - } - }], - links: { - first: "http://localhost/post?page%5Blimit%5D=100", - last: "http://localhost/post?page%5Boffset%5D=0", - next: null, - prev: null, - self: "http://localhost/post" - }, - meta: { - total: 1 - } - }) - }) - - it('option baseUrl', async () => { - const { prisma, enhanceRaw } = await loadSchema(schema); - - await prisma.user.create({ - data: { - posts: { - create: [ - { title: 'post1', published: true }, - { title: 'post2', published: false }, - ], - }, - }, - }); - - const moduleRef = await Test.createTestingModule({ - imports: [ - ZenStackModule.registerAsync({ - useFactory: (prismaService) => ({ getEnhancedPrisma: () => enhanceRaw(prismaService) }), - inject: ['PrismaService'], - extraProviders: [ - { - provide: 'PrismaService', - useValue: prisma, - }, - ], - }), - ], - providers: [ - { - provide: REQUEST, - useValue: {} - }, - { - provide: HttpAdapterHost, - useValue: { - httpAdapter: { - getRequestHostname: jest.fn().mockReturnValue('localhost'), - getRequestUrl: jest.fn().mockReturnValue('/api/rpc/post/findMany'), - getRequestMethod: jest.fn().mockReturnValue('GET'), - } - } - }, - ApiHandlerService, - ], - }).compile(); - - const service = await moduleRef.resolve(ApiHandlerService); - expect(await service.handleRequest({ - baseUrl: '/api/rpc' - })).toEqual({ - data: [{ - id: 1, - title: 'post1', - published: true, - authorId: 1, - }] - }) - }) -}) diff --git a/packages/server/tests/adapter/next.test.ts b/packages/server/tests/adapter/next.test.ts deleted file mode 100644 index 733b30ade..000000000 --- a/packages/server/tests/adapter/next.test.ts +++ /dev/null @@ -1,313 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { loadSchema } from '@zenstackhq/testtools'; -import { createServer, RequestListener } from 'http'; -import { apiResolver } from 'next/dist/server/api-utils/node/api-resolver'; -import path from 'path'; -import request from 'supertest'; -import Rest from '../../src/api/rest'; -import { NextRequestHandler, RequestHandlerOptions } from '../../src/next'; - -function makeTestClient(apiPath: string, options: RequestHandlerOptions, qArg?: unknown, otherArgs?: any) { - const pathParts = apiPath.split('/').filter((p) => p); - - const query = { - path: pathParts, - ...(qArg ? { q: JSON.stringify(qArg) } : {}), - ...otherArgs, - }; - - const handler = NextRequestHandler(options); - - const listener: RequestListener = (req, res) => { - return apiResolver( - req, - res, - query, - handler, - { - previewModeEncryptionKey: '', - previewModeId: '', - previewModeSigningKey: '', - }, - false - ); - }; - - return request(createServer(listener)); -} - -describe('Next.js adapter tests - rpc handler', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('simple crud', async () => { - const model = ` -model M { - id String @id @default(cuid()) - value Int -} - `; - - const { prisma } = await loadSchema(model); - - await makeTestClient('/m/create', { getPrisma: () => prisma }) - .post('/') - .send({ data: { id: '1', value: 1 } }) - .expect(201) - .expect((resp) => { - expect(resp.body.data.value).toBe(1); - }); - - await makeTestClient('/m/findUnique', { getPrisma: () => prisma }, { where: { id: '1' } }) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data.value).toBe(1); - }); - - await makeTestClient('/m/findFirst', { getPrisma: () => prisma }, { where: { id: '1' } }) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data.value).toBe(1); - }); - - await makeTestClient('/m/findMany', { getPrisma: () => prisma }, {}) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data).toHaveLength(1); - }); - - await makeTestClient('/m/update', { getPrisma: () => prisma }) - .put('/') - .send({ where: { id: '1' }, data: { value: 2 } }) - .expect(200) - .expect((resp) => { - expect(resp.body.data.value).toBe(2); - }); - - await makeTestClient('/m/updateMany', { getPrisma: () => prisma }) - .put('/') - .send({ data: { value: 4 } }) - .expect(200) - .expect((resp) => { - expect(resp.body.data.count).toBe(1); - }); - - await makeTestClient('/m/upsert', { getPrisma: () => prisma }) - .post('/') - .send({ where: { id: '2' }, create: { id: '2', value: 2 }, update: { value: 3 } }) - .expect(201) - .expect((resp) => { - expect(resp.body.data.value).toBe(2); - }); - - await makeTestClient('/m/upsert', { getPrisma: () => prisma }) - .post('/') - .send({ where: { id: '2' }, create: { id: '2', value: 2 }, update: { value: 3 } }) - .expect(201) - .expect((resp) => { - expect(resp.body.data.value).toBe(3); - }); - - await makeTestClient('/m/count', { getPrisma: () => prisma }, { where: { id: '1' } }) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data).toBe(1); - }); - - await makeTestClient('/m/count', { getPrisma: () => prisma }, {}) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data).toBe(2); - }); - - await makeTestClient('/m/aggregate', { getPrisma: () => prisma }, { _sum: { value: true } }) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data._sum.value).toBe(7); - }); - - await makeTestClient('/m/groupBy', { getPrisma: () => prisma }, { by: ['id'], _sum: { value: true } }) - .get('/') - .expect(200) - .expect((resp) => { - const data = resp.body.data; - expect(data).toHaveLength(2); - expect(data.find((item: any) => item.id === '1')._sum.value).toBe(4); - expect(data.find((item: any) => item.id === '2')._sum.value).toBe(3); - }); - - await makeTestClient('/m/delete', { getPrisma: () => prisma }, { where: { id: '1' } }) - .del('/') - .expect(200); - expect(await prisma.m.count()).toBe(1); - - await makeTestClient('/m/deleteMany', { getPrisma: () => prisma }, {}) - .del('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data.count).toBe(1); - }); - expect(await prisma.m.count()).toBe(0); - }); - - it('custom load path', async () => { - const model = ` -model M { - id String @id @default(cuid()) - value Int -} - `; - - const { prisma, projectDir } = await loadSchema(model, { output: './zen' }); - - await makeTestClient('/m/create', { - getPrisma: () => prisma, - modelMeta: require(path.join(projectDir, './zen/model-meta')).default, - zodSchemas: require(path.join(projectDir, './zen/zod')), - }) - .post('/') - .send({ data: { id: '1', value: 1 } }) - .expect(201) - .expect((resp) => { - expect(resp.body.data.value).toBe(1); - }); - }); - - it('access policy crud', async () => { - const model = ` -model M { - id String @id @default(cuid()) - value Int - - @@allow('create', true) - @@allow('read', value > 0) - @@allow('update', future().value > 1) - @@allow('delete', value > 2) -} - `; - - const { enhance } = await loadSchema(model); - - await makeTestClient('/m/create', { getPrisma: () => enhance() }) - .post('/m/create') - .send({ data: { value: 0 } }) - .expect(403) - .expect((resp) => { - expect(resp.body.error.reason).toBe('RESULT_NOT_READABLE'); - }); - - await makeTestClient('/m/create', { getPrisma: () => enhance() }) - .post('/') - .send({ data: { id: '1', value: 1 } }) - .expect(201); - - await makeTestClient('/m/findMany', { getPrisma: () => enhance() }) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data).toHaveLength(1); - }); - - await makeTestClient('/m/update', { getPrisma: () => enhance() }) - .put('/') - .send({ where: { id: '1' }, data: { value: 0 } }) - .expect(403); - - await makeTestClient('/m/update', { getPrisma: () => enhance() }) - .put('/') - .send({ where: { id: '1' }, data: { value: 2 } }) - .expect(200); - - await makeTestClient('/m/delete', { getPrisma: () => enhance() }, { where: { id: '1' } }) - .del('/') - .expect(403); - - await makeTestClient('/m/update', { getPrisma: () => enhance() }) - .put('/') - .send({ where: { id: '1' }, data: { value: 3 } }) - .expect(200); - - await makeTestClient('/m/delete', { getPrisma: () => enhance() }, { where: { id: '1' } }) - .del('/') - .expect(200); - }); -}); - -describe('Next.js adapter tests - rest handler', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('adapter test - rest', async () => { - const model = ` -model M { - id String @id @default(cuid()) - value Int -} - `; - - const { prisma, modelMeta } = await loadSchema(model); - - const options = { getPrisma: () => prisma, handler: Rest({ endpoint: 'http://localhost/api' }), modelMeta }; - - await makeTestClient('/m', options) - .post('/') - .send({ data: { type: 'm', attributes: { id: '1', value: 1 } } }) - .expect(201) - .expect((resp) => { - expect(resp.body.data.attributes.value).toBe(1); - }); - - await makeTestClient('/m/1', options) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data.id).toBe('1'); - }); - - await makeTestClient('/m', options, undefined, { 'filter[value]': '1' }) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data).toHaveLength(1); - }); - - await makeTestClient('/m', options, undefined, { 'filter[value]': '2' }) - .get('/') - .expect(200) - .expect((resp) => { - expect(resp.body.data).toHaveLength(0); - }); - - await makeTestClient('/m/1', options) - .put('/') - .send({ data: { type: 'm', attributes: { value: 2 } } }) - .expect(200) - .expect((resp) => { - expect(resp.body.data.attributes.value).toBe(2); - }); - - await makeTestClient('/m/1', options).del('/').expect(200); - expect(await prisma.m.count()).toBe(0); - }); -}); diff --git a/packages/server/tests/adapter/sveltekit.test.ts b/packages/server/tests/adapter/sveltekit.test.ts deleted file mode 100644 index 41621a469..000000000 --- a/packages/server/tests/adapter/sveltekit.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// -import { loadSchema } from '@zenstackhq/testtools'; -import 'isomorphic-fetch'; -import path from 'path'; -import superjson from 'superjson'; -import Rest from '../../src/api/rest'; -import { SvelteKitHandler } from '../../src/sveltekit'; -import { makeUrl, schema } from '../utils'; - -describe('SvelteKit adapter tests - rpc handler', () => { - it('run hooks regular json', async () => { - const { prisma, zodSchemas } = await loadSchema(schema); - - const handler = SvelteKitHandler({ prefix: '/api', getPrisma: () => prisma, zodSchemas }); - - let r = await handler(makeRequest('GET', makeUrl('/api/post/findMany', { where: { id: { equals: '1' } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler( - makeRequest('POST', '/api/user/create', { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }) - ); - expect(r.status).toBe(201); - expect((await unmarshal(r)).data).toMatchObject({ - email: 'user1@abc.com', - posts: expect.arrayContaining([ - expect.objectContaining({ title: 'post1' }), - expect.objectContaining({ title: 'post2' }), - ]), - }); - - r = await handler(makeRequest('GET', makeUrl('/api/post/findMany'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(2); - - r = await handler(makeRequest('GET', makeUrl('/api/post/findMany', { where: { viewCount: { gt: 1 } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(1); - - r = await handler( - makeRequest('PUT', '/api/user/update', { where: { id: 'user1' }, data: { email: 'user1@def.com' } }) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.email).toBe('user1@def.com'); - - r = await handler(makeRequest('GET', makeUrl('/api/post/count', { where: { viewCount: { gt: 1 } } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toBe(1); - - r = await handler(makeRequest('GET', makeUrl('/api/post/aggregate', { _sum: { viewCount: true } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data._sum.viewCount).toBe(3); - - r = await handler( - makeRequest('GET', makeUrl('/api/post/groupBy', { by: ['published'], _sum: { viewCount: true } })) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toEqual( - expect.arrayContaining([ - expect.objectContaining({ published: true, _sum: { viewCount: 1 } }), - expect.objectContaining({ published: false, _sum: { viewCount: 2 } }), - ]) - ); - - r = await handler(makeRequest('DELETE', makeUrl('/api/user/deleteMany', { where: { id: 'user1' } }))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.count).toBe(1); - }); - - // TODO: investigate failure in CI - // eslint-disable-next-line jest/no-disabled-tests - it.skip('custom load path', async () => { - const { prisma, projectDir } = await loadSchema(schema, { output: './zen' }); - - const handler = SvelteKitHandler({ - prefix: '/api', - getPrisma: () => prisma, - modelMeta: require(path.join(projectDir, './zen/model-meta')).default, - zodSchemas: require(path.join(projectDir, './zen/zod')), - }); - - const r = await handler( - makeRequest('POST', '/api/user/create', { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }) - ); - expect(r.status).toBe(201); - }); -}); - -describe('SvelteKit adapter tests - rest handler', () => { - it('run hooks', async () => { - const { prisma, modelMeta, zodSchemas } = await loadSchema(schema); - - const handler = SvelteKitHandler({ - prefix: '/api', - getPrisma: () => prisma, - handler: Rest({ endpoint: 'http://localhost/api' }), - modelMeta, - zodSchemas, - }); - - let r = await handler(makeRequest('GET', makeUrl('/api/post/1'))); - expect(r.status).toBe(404); - - r = await handler( - makeRequest('POST', '/api/user', { - data: { - type: 'user', - attributes: { id: 'user1', email: 'user1@abc.com' }, - }, - }) - ); - expect(r.status).toBe(201); - expect(await unmarshal(r)).toMatchObject({ - data: { - id: 'user1', - attributes: { - email: 'user1@abc.com', - }, - }, - }); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user1'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(1); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user2'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler(makeRequest('GET', makeUrl('/api/user?filter[id]=user1&filter[email]=xyz'))); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data).toHaveLength(0); - - r = await handler( - makeRequest('PUT', makeUrl('/api/user/user1'), { - data: { type: 'user', attributes: { email: 'user1@def.com' } }, - }) - ); - expect(r.status).toBe(200); - expect((await unmarshal(r)).data.attributes.email).toBe('user1@def.com'); - - r = await handler(makeRequest('DELETE', makeUrl('/api/user/user1'))); - expect(r.status).toBe(200); - expect(await prisma.user.findMany()).toHaveLength(0); - }); -}); - -function makeRequest(method: string, path: string, body?: any) { - const payload = body ? JSON.stringify(body) : undefined; - return { - event: { - request: new Request(`http://localhost${path}`, { method, body: payload }), - url: new URL(`http://localhost${path}`), - } as any, - resolve: async () => { - throw new Error('should not be called'); - }, - }; -} - -async function unmarshal(r: Response, useSuperJson = false) { - const text = await r.text(); - return (useSuperJson ? superjson.parse(text) : JSON.parse(text)) as any; -} diff --git a/packages/server/tests/adapter/tanstack-start.test.ts b/packages/server/tests/adapter/tanstack-start.test.ts deleted file mode 100644 index db34c1ca7..000000000 --- a/packages/server/tests/adapter/tanstack-start.test.ts +++ /dev/null @@ -1,269 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; -import Rest from '../../src/api/rest'; -import { TanStackStartHandler, TanStackStartOptions } from '../../src/tanstack-start'; - -function makeRequest(method: string, url: string, body?: any): Request { - const payload = body ? JSON.stringify(body) : undefined; - return new Request(url, { method, body: payload }); -} - -async function unmarshal(response: Response): Promise { - const text = await response.text(); - return JSON.parse(text); -} - -interface TestClient { - get: () => Promise<{ status: number; body: any }>; - post: () => { send: (data: any) => Promise<{ status: number; body: any }> }; - put: () => { send: (data: any) => Promise<{ status: number; body: any }> }; - del: () => Promise<{ status: number; body: any }>; -} - -function makeTestClient(apiPath: string, options: TanStackStartOptions, qArg?: unknown, otherArgs?: any): TestClient { - const pathParts = apiPath.split('/').filter((p) => p); - const path = pathParts.join('/'); - - const handler = TanStackStartHandler(options); - - const params = { - _splat: path, - ...otherArgs, - }; - - const buildUrl = (method: string) => { - const baseUrl = `http://localhost${apiPath}`; - if (method === 'GET' || method === 'DELETE') { - const url = new URL(baseUrl); - if (qArg) { - url.searchParams.set('q', JSON.stringify(qArg)); - } - if (otherArgs) { - Object.entries(otherArgs).forEach(([key, value]) => { - url.searchParams.set(key, String(value)); - }); - } - return url.toString(); - } - return baseUrl; - }; - - const executeRequest = async (method: string, body?: any) => { - const url = buildUrl(method); - const request = makeRequest(method, url, body); - const response = await handler({ request, params }); - const responseBody = await unmarshal(response); - return { - status: response.status, - body: responseBody, - }; - }; - - return { - get: async () => executeRequest('GET'), - post: () => ({ - send: async (data: any) => executeRequest('POST', data), - }), - put: () => ({ - send: async (data: any) => executeRequest('PUT', data), - }), - del: async () => executeRequest('DELETE'), - }; -} - -describe('TanStack Start adapter tests - rpc handler', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('simple crud', async () => { - const model = ` -model M { - id String @id @default(cuid()) - value Int -} - `; - - const { prisma } = await loadSchema(model); - - const client = await makeTestClient('/m/create', { getPrisma: () => prisma }).post().send({ data: { id: '1', value: 1 } }); - expect(client.status).toBe(201); - expect(client.body.data.value).toBe(1); - - const findUnique = await makeTestClient('/m/findUnique', { getPrisma: () => prisma }, { where: { id: '1' } }).get(); - expect(findUnique.status).toBe(200); - expect(findUnique.body.data.value).toBe(1); - - const findFirst = await makeTestClient('/m/findFirst', { getPrisma: () => prisma }, { where: { id: '1' } }).get(); - expect(findFirst.status).toBe(200); - expect(findFirst.body.data.value).toBe(1); - - const findMany = await makeTestClient('/m/findMany', { getPrisma: () => prisma }, {}).get(); - expect(findMany.status).toBe(200); - expect(findMany.body.data).toHaveLength(1); - - const update = await makeTestClient('/m/update', { getPrisma: () => prisma }).put().send({ where: { id: '1' }, data: { value: 2 } }); - expect(update.status).toBe(200); - expect(update.body.data.value).toBe(2); - - const updateMany = await makeTestClient('/m/updateMany', { getPrisma: () => prisma }).put().send({ data: { value: 4 } }); - expect(updateMany.status).toBe(200); - expect(updateMany.body.data.count).toBe(1); - - const upsert1 = await makeTestClient('/m/upsert', { getPrisma: () => prisma }).post().send({ where: { id: '2' }, create: { id: '2', value: 2 }, update: { value: 3 } }); - expect(upsert1.status).toBe(201); - expect(upsert1.body.data.value).toBe(2); - - const upsert2 = await makeTestClient('/m/upsert', { getPrisma: () => prisma }).post().send({ where: { id: '2' }, create: { id: '2', value: 2 }, update: { value: 3 } }); - expect(upsert2.status).toBe(201); - expect(upsert2.body.data.value).toBe(3); - - const count1 = await makeTestClient('/m/count', { getPrisma: () => prisma }, { where: { id: '1' } }).get(); - expect(count1.status).toBe(200); - expect(count1.body.data).toBe(1); - - const count2 = await makeTestClient('/m/count', { getPrisma: () => prisma }, {}).get(); - expect(count2.status).toBe(200); - expect(count2.body.data).toBe(2); - - const aggregate = await makeTestClient('/m/aggregate', { getPrisma: () => prisma }, { _sum: { value: true } }).get(); - expect(aggregate.status).toBe(200); - expect(aggregate.body.data._sum.value).toBe(7); - - const groupBy = await makeTestClient('/m/groupBy', { getPrisma: () => prisma }, { by: ['id'], _sum: { value: true } }).get(); - expect(groupBy.status).toBe(200); - const data = groupBy.body.data; - expect(data).toHaveLength(2); - expect(data.find((item: any) => item.id === '1')._sum.value).toBe(4); - expect(data.find((item: any) => item.id === '2')._sum.value).toBe(3); - - const deleteOne = await makeTestClient('/m/delete', { getPrisma: () => prisma }, { where: { id: '1' } }).del(); - expect(deleteOne.status).toBe(200); - expect(await prisma.m.count()).toBe(1); - - const deleteMany = await makeTestClient('/m/deleteMany', { getPrisma: () => prisma }, {}).del(); - expect(deleteMany.status).toBe(200); - expect(deleteMany.body.data.count).toBe(1); - expect(await prisma.m.count()).toBe(0); - }); - - it('custom load path', async () => { - const model = ` -model M { - id String @id @default(cuid()) - value Int -} - `; - - const { prisma, projectDir } = await loadSchema(model, { output: './zen' }); - - const client = await makeTestClient('/m/create', { - getPrisma: () => prisma, - modelMeta: require(path.join(projectDir, './zen/model-meta')).default, - zodSchemas: require(path.join(projectDir, './zen/zod')), - }).post().send({ data: { id: '1', value: 1 } }); - - expect(client.status).toBe(201); - expect(client.body.data.value).toBe(1); - }); - - it('access policy crud', async () => { - const model = ` -model M { - id String @id @default(cuid()) - value Int - - @@allow('create', true) - @@allow('read', value > 0) - @@allow('update', future().value > 1) - @@allow('delete', value > 2) -} - `; - - const { enhance } = await loadSchema(model); - - const createForbidden = await makeTestClient('/m/create', { getPrisma: () => enhance() }).post().send({ data: { value: 0 } }); - expect(createForbidden.status).toBe(403); - expect(createForbidden.body.error.reason).toBe('RESULT_NOT_READABLE'); - - const create = await makeTestClient('/m/create', { getPrisma: () => enhance() }).post().send({ data: { id: '1', value: 1 } }); - expect(create.status).toBe(201); - - const findMany = await makeTestClient('/m/findMany', { getPrisma: () => enhance() }).get(); - expect(findMany.status).toBe(200); - expect(findMany.body.data).toHaveLength(1); - - const updateForbidden1 = await makeTestClient('/m/update', { getPrisma: () => enhance() }).put().send({ where: { id: '1' }, data: { value: 0 } }); - expect(updateForbidden1.status).toBe(403); - - const update1 = await makeTestClient('/m/update', { getPrisma: () => enhance() }).put().send({ where: { id: '1' }, data: { value: 2 } }); - expect(update1.status).toBe(200); - - const deleteForbidden = await makeTestClient('/m/delete', { getPrisma: () => enhance() }, { where: { id: '1' } }).del(); - expect(deleteForbidden.status).toBe(403); - - const update2 = await makeTestClient('/m/update', { getPrisma: () => enhance() }).put().send({ where: { id: '1' }, data: { value: 3 } }); - expect(update2.status).toBe(200); - - const deleteOne = await makeTestClient('/m/delete', { getPrisma: () => enhance() }, { where: { id: '1' } }).del(); - expect(deleteOne.status).toBe(200); - }); -}); - -describe('TanStack Start adapter tests - rest handler', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('adapter test - rest', async () => { - const model = ` -model M { - id String @id @default(cuid()) - value Int -} - `; - - const { prisma, modelMeta } = await loadSchema(model); - - const options = { getPrisma: () => prisma, handler: Rest({ endpoint: 'http://localhost/api' }), modelMeta }; - - const create = await makeTestClient('/m', options).post().send({ data: { type: 'm', attributes: { id: '1', value: 1 } } }); - expect(create.status).toBe(201); - expect(create.body.data.attributes.value).toBe(1); - - const getOne = await makeTestClient('/m/1', options).get(); - expect(getOne.status).toBe(200); - expect(getOne.body.data.id).toBe('1'); - - const findWithFilter1 = await makeTestClient('/m', options, undefined, { 'filter[value]': '1' }).get(); - expect(findWithFilter1.status).toBe(200); - expect(findWithFilter1.body.data).toHaveLength(1); - - const findWithFilter2 = await makeTestClient('/m', options, undefined, { 'filter[value]': '2' }).get(); - expect(findWithFilter2.status).toBe(200); - expect(findWithFilter2.body.data).toHaveLength(0); - - const update = await makeTestClient('/m/1', options).put().send({ data: { type: 'm', attributes: { value: 2 } } }); - expect(update.status).toBe(200); - expect(update.body.data.attributes.value).toBe(2); - - const deleteOne = await makeTestClient('/m/1', options).del(); - expect(deleteOne.status).toBe(200); - expect(await prisma.m.count()).toBe(0); - }); -}); - diff --git a/packages/server/tests/api/rest-partial.test.ts b/packages/server/tests/api/rest-partial.test.ts deleted file mode 100644 index ac4a28f50..000000000 --- a/packages/server/tests/api/rest-partial.test.ts +++ /dev/null @@ -1,473 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import { type ModelMeta } from '@zenstackhq/runtime'; -import { loadSchema, run } from '@zenstackhq/testtools'; -import makeHandler from '../../src/api/rest'; - -describe('REST server tests', () => { - let prisma: any; - let zodSchemas: any; - let modelMeta: ModelMeta; - let handler: (any: any) => Promise<{ status: number; body: any }>; - - beforeEach(async () => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - }); - - describe('REST server tests - sparse fieldsets', () => { - const schema = ` - model User { - myId String @id @default(cuid()) - createdAt DateTime @default (now()) - updatedAt DateTime @updatedAt - email String @unique @email - nickName String - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - createdAt DateTime @default (now()) - updatedAt DateTime @updatedAt - title String @length(1, 10) - content String - author User? @relation(fields: [authorId], references: [myId]) - authorId String? - published Boolean @default(false) - publishedAt DateTime? - viewCount Int @default(0) - comments Comment[] - } - - model Comment { - id Int @id @default(autoincrement()) - post Post @relation(fields: [postId], references: [id]) - postId Int - content String - } - `; - - beforeAll(async () => { - const params = await loadSchema(schema); - - prisma = params.prisma; - zodSchemas = params.zodSchemas; - modelMeta = params.modelMeta; - - const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5 }); - handler = (args) => - _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - }); - - it('returns only the requested fields when there are some in the database', async () => { - // Create users first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'one', - posts: { - create: { title: 'Post1', content: 'Post 1 Content' }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - nickName: 'two', - posts: { - create: { title: 'Post2', content: 'Post 2 Content' }, - }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user', - prisma, - query: { ['fields[user]']: 'email,nickName' }, - }); - - expect(r.status).toBe(200); - - expect(r.body.data[0].attributes).toEqual({ - email: 'user1@abc.com', - nickName: 'one', - }); - - expect(r.body.data[1].attributes).toEqual({ - email: 'user2@abc.com', - nickName: 'two', - }); - }); - - it('returns collection with only the requested fields when there are includes', async () => { - // Create users first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'one', - posts: { - create: { title: 'Post1', content: 'Post 1 Content' }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - nickName: 'two', - posts: { - create: { title: 'Post2', content: 'Post 2 Content', published: true }, - }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user', - prisma, - query: { ['fields[user]']: 'email,nickName', ['fields[post]']: 'title,published', include: 'posts' }, - }); - - expect(r.status).toBe(200); - - expect(r.body.data[0].attributes).toEqual({ - email: 'user1@abc.com', - nickName: 'one', - }); - - expect(r.body.data[1].attributes).toEqual({ - email: 'user2@abc.com', - nickName: 'two', - }); - - expect(r.body.included[0].attributes).toEqual({ - title: 'Post1', - published: false, - }); - - expect(r.body.included[1].attributes).toEqual({ - title: 'Post2', - published: true, - }); - }); - - it('returns collection with only the requested fields when there are deep includes', async () => { - // Create users first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'one', - posts: { - create: { - title: 'Post1', - content: 'Post 1 Content', - comments: { create: { content: 'Comment1' } }, - }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - nickName: 'two', - posts: { - create: { - title: 'Post2', - content: 'Post 2 Content', - published: true, - comments: { create: { content: 'Comment2' } }, - }, - }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user', - prisma, - query: { - ['fields[user]']: 'email,nickName', - ['fields[post]']: 'title,published', - ['fields[comment]']: 'content', - include: 'posts,posts.comments', - }, - }); - - expect(r.status).toBe(200); - - expect(r.body.data[0].attributes).toEqual({ - email: 'user1@abc.com', - nickName: 'one', - }); - - expect(r.body.data[1].attributes).toEqual({ - email: 'user2@abc.com', - nickName: 'two', - }); - - expect(r.body.included[0].attributes).toEqual({ - title: 'Post1', - published: false, - }); - - expect(r.body.included[1].attributes).toEqual({ - title: 'Post2', - published: true, - }); - - expect(r.body.included[2].attributes).toEqual({ content: 'Comment1' }); - expect(r.body.included[3].attributes).toEqual({ content: 'Comment2' }); - }); - - it('returns collection with only the requested fields when there are sparse fields on deep includes', async () => { - // Create users first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'one', - posts: { - create: { - title: 'Post1', - content: 'Post 1 Content', - comments: { create: { content: 'Comment1' } }, - }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - nickName: 'two', - posts: { - create: { - title: 'Post2', - content: 'Post 2 Content', - published: true, - comments: { create: { content: 'Comment2' } }, - }, - }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user', - prisma, - query: { - ['fields[user]']: 'email,nickName', - ['fields[comment]']: 'content', - include: 'posts,posts.comments', - }, - }); - - expect(r.status).toBe(200); - - expect(r.body.data[0].attributes).toEqual({ - email: 'user1@abc.com', - nickName: 'one', - }); - - expect(r.body.data[1].attributes).toEqual({ - email: 'user2@abc.com', - nickName: 'two', - }); - - //did not use sparse field on posts, only comments - expect(r.body.included[0].attributes).toMatchObject({ - title: 'Post1', - published: false, - }); - - //did not use sparse field on posts, only comments - expect(r.body.included[1].attributes).toMatchObject({ - title: 'Post2', - published: true, - }); - - expect(r.body.included[2].attributes).toEqual({ content: 'Comment1' }); - expect(r.body.included[3].attributes).toEqual({ content: 'Comment2' }); - }); - - it('returns only the requested fields when the ID is specified', async () => { - // Create a user first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'User 1', - posts: { create: { title: 'Post1', content: 'Post 1 Content' } }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user/user1', - prisma, - query: { ['fields[user]']: 'email' }, - }); - - expect(r.status).toBe(200); - expect(r.body.data.attributes).toEqual({ email: 'user1@abc.com' }); - }); - - it('returns only the requested fields when the ID is specified and has an include', async () => { - // Create a user first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'User 1', - posts: { create: { title: 'Post1', content: 'Post 1 Content' } }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user/user1', - prisma, - query: { ['fields[user]']: 'email,nickName', ['fields[post]']: 'title,published', include: 'posts' }, - }); - - expect(r.status).toBe(200); - expect(r.body.data.attributes).toEqual({ email: 'user1@abc.com', nickName: 'User 1' }); - - expect(r.body.included[0].attributes).toEqual({ - title: 'Post1', - published: false, - }); - }); - - it('fetch only requested fields on a related resource', async () => { - // Create a user first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'one', - posts: { - create: { title: 'Post1', content: 'Post 1 Content' }, - }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user/user1/posts', - prisma, - query: { ['fields[post]']: 'title,content' }, - }); - - expect(r.status).toBe(200); - expect(r.body.data[0].attributes).toEqual({ - title: 'Post1', - content: 'Post 1 Content', - }); - }); - - it('does not efect toplevel filtering', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'one', - posts: { - create: { id: 1, title: 'Post1', content: 'Post 1 Content' }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - nickName: 'two', - posts: { - create: { id: 2, title: 'Post2', content: 'Post 2 Content', viewCount: 1, published: true }, - }, - }, - }); - - // id filter - const r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[id]']: 'user2', ['fields[user]']: 'email' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 'user2' }); - expect(r.body.data[0].attributes).not.toMatchObject({ nickName: 'two' }); - }); - - it('does not efect toplevel sorting', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - nickName: 'one', - posts: { - create: { id: 1, title: 'Post1', content: 'Post 1 Content', viewCount: 1, published: true }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - nickName: 'two', - posts: { - create: { id: 2, title: 'Post2', content: 'Post 2 Content', viewCount: 2, published: false }, - }, - }, - }); - - // basic sorting - const r = await handler({ - method: 'get', - path: '/post', - query: { sort: 'viewCount', ['fields[post]']: 'title' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 1 }); - }); - - it('does not efect toplevel pagination', async () => { - for (const i of Array(5).keys()) { - await prisma.user.create({ - data: { - myId: `user${i}`, - email: `user${i}@abc.com`, - nickName: `{i}`, - }, - }); - } - - // limit only - const r = await handler({ - method: 'get', - path: '/user', - query: { ['page[limit]']: '3', ['fields[user]']: 'email' }, - prisma, - }); - expect(r.body.data).toHaveLength(3); - expect(r.body.meta.total).toBe(5); - expect(r.body.links).toMatchObject({ - first: 'http://localhost/api/user?fields%5Buser%5D=email&page%5Blimit%5D=3', - last: 'http://localhost/api/user?fields%5Buser%5D=email&page%5Boffset%5D=3', - prev: null, - next: 'http://localhost/api/user?fields%5Buser%5D=email&page%5Boffset%5D=3&page%5Blimit%5D=3', - }); - }); - }); -}); diff --git a/packages/server/tests/api/rest-petstore.test.ts b/packages/server/tests/api/rest-petstore.test.ts deleted file mode 100644 index 743b18fe6..000000000 --- a/packages/server/tests/api/rest-petstore.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import { ModelMeta } from '@zenstackhq/runtime'; -import { loadSchema, run } from '@zenstackhq/testtools'; -import makeHandler from '../../src/api/rest'; -import { Response } from '../../src/types'; - -let prisma: any; -let zodSchemas: any; -let modelMeta: ModelMeta; -let db: any; -let handler: (any: any) => Promise; - -describe('REST server tests - Pet Store API', () => { - const schema = ` - model User { - id String @id @default(cuid()) - email String @unique - orders Order[] - - // everybody can signup - @@allow('create', true) - - // user profile is publicly readable - @@allow('read', true) - } - - model Pet { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String - category String - order Order? @relation(fields: [orderId], references: [id]) - orderId String? - - // unsold pets are readable to all; sold ones are readable to buyers only - @@allow('read', orderId == null || order.user == auth()) - - // only allow update to 'orderId' field if it's not set yet (unsold) - @@allow('update', name == future().name && category == future().category && orderId == null ) - } - - model Order { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - pets Pet[] - user User @relation(fields: [userId], references: [id]) - userId String - - // users can read their orders - @@allow('read,create', auth() == user) - } - `; - - beforeAll(async () => { - const params = await loadSchema(schema); - - prisma = params.prisma; - db = params.enhance({ id: 'user1' }); - zodSchemas = params.zodSchemas; - modelMeta = params.modelMeta; - - const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5 }); - handler = (args) => _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - }); - - beforeEach(async () => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - - const petData = [ - { - id: 'luna', - name: 'Luna', - category: 'kitten', - }, - { - id: 'max', - name: 'Max', - category: 'doggie', - }, - { - id: 'cooper', - name: 'Cooper', - category: 'reptile', - }, - ]; - - for (const pet of petData) { - await prisma.pet.create({ data: pet }); - } - - await prisma.user.create({ data: { id: 'user1', email: 'user1@abc.com' } }); - }); - - it('crud test', async () => { - const r = await handler({ - method: 'post', - path: '/order', - prisma: db, - requestBody: { - data: { - type: 'order', - relationships: { - user: { data: { type: 'user', id: 'user1' } }, - pets: { data: [{ type: 'pet', id: 'luna' }] }, - }, - }, - }, - }); - expect(r.status).toBe(201); - expect((r.body as any).data.relationships.user.data.id).toBe('user1'); - expect((r.body as any).data.relationships.pets.data[0].id).toBe('luna'); - }); -}); diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts deleted file mode 100644 index 116da8c59..000000000 --- a/packages/server/tests/api/rest.test.ts +++ /dev/null @@ -1,3197 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import { CrudFailureReason, type ModelMeta } from '@zenstackhq/runtime'; -import { createPostgresDb, dropPostgresDb, loadSchema, run } from '@zenstackhq/testtools'; -import { Decimal } from 'decimal.js'; -import SuperJSON from 'superjson'; -import makeHandler from '../../src/api/rest'; - -const idDivider = '_'; - -describe('REST server tests', () => { - let prisma: any; - let zodSchemas: any; - let modelMeta: ModelMeta; - let handler: (any: any) => Promise<{ status: number; body: any }>; - - beforeEach(async () => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - }); - - describe('REST server tests - regular prisma', () => { - const schema = ` - type Address { - city String - } - - model User { - myId String @id @default(cuid()) - createdAt DateTime @default (now()) - updatedAt DateTime @updatedAt - email String @unique @email - posts Post[] - likes PostLike[] - profile Profile? - address Address? @json - someJson Json? - } - - model Profile { - id Int @id @default(autoincrement()) - gender String - user User @relation(fields: [userId], references: [myId]) - userId String @unique - } - - model Post { - id Int @id @default(autoincrement()) - createdAt DateTime @default (now()) - updatedAt DateTime @updatedAt - title String @length(1, 10) - author User? @relation(fields: [authorId], references: [myId]) - authorId String? - published Boolean @default(false) - publishedAt DateTime? - viewCount Int @default(0) - comments Comment[] - likes PostLike[] - setting Setting? - } - - model Comment { - id Int @id @default(autoincrement()) - post Post @relation(fields: [postId], references: [id]) - postId Int - content String - } - - model Setting { - id Int @id @default(autoincrement()) - boost Int - post Post @relation(fields: [postId], references: [id]) - postId Int @unique - } - - model PostLike { - postId Int - userId String - superLike Boolean - post Post @relation(fields: [postId], references: [id]) - user User @relation(fields: [userId], references: [myId]) - likeInfos PostLikeInfo[] - @@id([postId, userId]) - } - - model PostLikeInfo { - id Int @id @default(autoincrement()) - text String - postId Int - userId String - postLike PostLike @relation(fields: [postId, userId], references: [postId, userId]) - } - `; - - beforeAll(async () => { - const params = await loadSchema(schema); - - prisma = params.prisma; - zodSchemas = params.zodSchemas; - modelMeta = params.modelMeta; - - const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5 }); - handler = (args) => - _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - }); - - describe('CRUD', () => { - describe('GET', () => { - it('invalid type, id, relationship', async () => { - let r = await handler({ - method: 'get', - path: '/foo', - prisma, - }); - expect(r.status).toBe(404); - - r = await handler({ - method: 'get', - path: '/user/user1/posts', - prisma, - }); - expect(r.status).toBe(404); - - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { title: 'Post1' }, - }, - }, - }); - - r = await handler({ - method: 'get', - path: '/user/user1/relationships/foo', - prisma, - }); - expect(r.status).toBe(404); - - r = await handler({ - method: 'get', - path: '/user/user1/foo', - prisma, - }); - expect(r.status).toBe(404); - }); - - it('returns an empty array when no item exists', async () => { - const r = await handler({ - method: 'get', - path: '/user', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - data: [], - links: { - self: 'http://localhost/api/user', - }, - }); - }); - - it('returns all items when there are some in the database', async () => { - // Create users first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { title: 'Post1' }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - posts: { - create: { title: 'Post2' }, - }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/user', - }, - meta: { - total: 2, - }, - data: [ - { - type: 'user', - id: 'user1', - attributes: { email: 'user1@abc.com' }, - links: { - self: 'http://localhost/api/user/user1', - }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - related: 'http://localhost/api/user/user1/posts', - }, - data: [{ type: 'post', id: 1 }], - }, - }, - }, - { - type: 'user', - id: 'user2', - attributes: { email: 'user2@abc.com' }, - links: { - self: 'http://localhost/api/user/user2', - }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user2/relationships/posts', - related: 'http://localhost/api/user/user2/posts', - }, - data: [{ type: 'post', id: 2 }], - }, - }, - }, - ], - }); - }); - - it('returns a single item when the ID is specified', async () => { - // Create a user first - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { title: 'Post1' } } }, - }); - - const r = await handler({ - method: 'get', - path: '/user/user1', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - data: { - type: 'user', - id: 'user1', - attributes: { email: 'user1@abc.com' }, - links: { - self: 'http://localhost/api/user/user1', - }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - related: 'http://localhost/api/user/user1/posts', - }, - data: [{ type: 'post', id: 1 }], - }, - }, - }, - }); - }); - - it('fetch a related resource', async () => { - // Create a user first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { id: 1, title: 'Post1' }, - }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user/user1/posts', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/user/user1/posts', - }, - data: [ - { - type: 'post', - id: 1, - attributes: { - title: 'Post1', - authorId: 'user1', - published: false, - viewCount: 0, - }, - links: { - self: 'http://localhost/api/post/1', - }, - relationships: { - author: { - links: { - self: 'http://localhost/api/post/1/relationships/author', - related: 'http://localhost/api/post/1/author', - }, - }, - }, - }, - ], - }); - }); - - it('returns an empty data array when loading empty related resources', async () => { - // Create a user first - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com' }, - }); - - const r = await handler({ - method: 'get', - path: '/user/user1', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - data: { - type: 'user', - id: 'user1', - attributes: { email: 'user1@abc.com' }, - links: { - self: 'http://localhost/api/user/user1', - }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - related: 'http://localhost/api/user/user1/posts', - }, - data: [], - }, - }, - }, - }); - }); - - it('fetches a related resource with a compound ID', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { id: 1, title: 'Post1' }, - }, - }, - }); - await prisma.postLike.create({ - data: { postId: 1, userId: 'user1', superLike: true }, - }); - - const r = await handler({ - method: 'get', - path: '/post/1/relationships/likes', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/post/1/relationships/likes', - }, - data: [{ type: 'postLike', id: `1${idDivider}user1` }], - }); - }); - - it('fetch a relationship', async () => { - // Create a user first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { id: 1, title: 'Post1' }, - }, - }, - }); - - const r = await handler({ - method: 'get', - path: '/user/user1/relationships/posts', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - }, - data: [{ type: 'post', id: 1 }], - }); - }); - - it('returns 404 if the specified ID does not exist', async () => { - const r = await handler({ - method: 'get', - path: '/user/nonexistentuser', - prisma, - }); - - expect(r.status).toBe(404); - expect(r.body).toEqual({ - errors: [ - { - code: 'not-found', - status: 404, - title: 'Resource not found', - }, - ], - }); - }); - - it('toplevel filtering', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - address: { city: 'Seattle' }, - someJson: 'foo', - posts: { - create: { id: 1, title: 'Post1' }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - posts: { - create: { id: 2, title: 'Post2', viewCount: 1, published: true }, - }, - }, - }); - - // id filter - let r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[id]']: 'user2' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 'user2' }); - - // multi-id filter - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[id]']: 'user1,user2' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(2); - - // String filter - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[email]']: 'user1@abc.com' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 'user1' }); - - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[email$contains]']: '1@abc' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 'user1' }); - - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[email$contains]']: '1@bc' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[email$startsWith]']: 'user1' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 'user1' }); - - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[email$startsWith]']: 'ser1' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[email$endsWith]']: '1@abc.com' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 'user1' }); - - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[email$endsWith]']: '1@abc' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - // Int filter - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[viewCount]']: '1' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[viewCount$gt]']: '0' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[viewCount$gte]']: '1' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[viewCount$lt]']: '0' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[viewCount$lte]']: '0' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 1 }); - - // Boolean filter - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[published]']: 'true' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - // deep to-one filter - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[author][email]']: 'user1@abc.com' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - - // deep to-many filter - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[posts][published]']: 'true' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - - // filter to empty - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[id]']: 'user3' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - // to-many relation collection filter - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[posts]']: '2' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 'user2' }); - - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[posts]']: '1,2,3' }, - prisma, - }); - expect(r.body.data).toHaveLength(2); - - // multi filter - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[id]']: 'user1', ['filter[posts]']: '2' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - r = await handler({ - method: 'get', - path: '/post', - query: { - ['filter[author][email]']: 'user1@abc.com', - ['filter[title]']: 'Post1', - }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - r = await handler({ - method: 'get', - path: '/post', - query: { - ['filter[author][email]']: 'user1@abc.com', - ['filter[title]']: 'Post2', - }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - // to-one relation filter - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[author]']: 'user1' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - expect(r.body.data[0]).toMatchObject({ id: 1 }); - - // relation filter with multiple values - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[author]']: 'user1,user2' }, - prisma, - }); - expect(r.body.data).toHaveLength(2); - - // invalid filter field - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[foo]']: '1' }, - prisma, - }); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-filter', - title: 'Invalid filter', - }, - ], - }); - - // invalid filter value - r = await handler({ - method: 'get', - path: '/post', - query: { ['filter[viewCount]']: 'a' }, - prisma, - }); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-value', - title: 'Invalid value for type', - }, - ], - }); - - // invalid filter operation - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[email$foo]']: '1' }, - prisma, - }); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-filter', - title: 'Invalid filter', - }, - ], - }); - - // typedef equality filter - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[address]']: JSON.stringify({ city: 'Seattle' }) }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[address]']: JSON.stringify({ city: 'Tokyo' }) }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - // plain json equality filter - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[someJson]']: JSON.stringify('foo') }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[someJson]']: JSON.stringify('bar') }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - // invalid json - r = await handler({ - method: 'get', - path: '/user', - query: { ['filter[someJson]']: '{ hello: world }' }, - prisma, - }); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-value', - title: 'Invalid value for type', - }, - ], - }); - }); - - it('related data filtering', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { id: 1, title: 'Post1' }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - posts: { - create: { id: 2, title: 'Post2', viewCount: 1, published: true }, - }, - }, - }); - - let r = await handler({ - method: 'get', - path: '/user/user1/posts', - query: { ['filter[viewCount]']: '1' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - r = await handler({ - method: 'get', - path: '/user/user2/posts', - query: { ['filter[viewCount]']: '1' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - }); - - it('relationship filtering', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { id: 1, title: 'Post1' }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - posts: { - create: { id: 2, title: 'Post2', viewCount: 1, published: true }, - }, - }, - }); - - let r = await handler({ - method: 'get', - path: '/user/user1/relationships/posts', - query: { ['filter[viewCount]']: '1' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - r = await handler({ - method: 'get', - path: '/user/user2/relationships/posts', - query: { ['filter[viewCount]']: '1' }, - prisma, - }); - expect(r.body.data).toHaveLength(1); - }); - - it('toplevel sorting', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { id: 1, title: 'Post1', viewCount: 1, published: true }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - posts: { - create: { id: 2, title: 'Post2', viewCount: 2, published: false }, - }, - }, - }); - - // basic sorting - let r = await handler({ - method: 'get', - path: '/post', - query: { sort: 'viewCount' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 1 }); - - // basic sorting desc - r = await handler({ - method: 'get', - path: '/post', - query: { sort: '-viewCount' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - // by relation id - r = await handler({ - method: 'get', - path: '/post', - query: { sort: '-author' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - // by relation field - r = await handler({ - method: 'get', - path: '/post', - query: { sort: '-author.email' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - // multi-field sorting - r = await handler({ - method: 'get', - path: '/post', - query: { sort: 'published,viewCount' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - r = await handler({ - method: 'get', - path: '/post', - query: { sort: 'viewCount,published' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 1 }); - - r = await handler({ - method: 'get', - path: '/post', - query: { sort: '-viewCount,-published' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - // invalid field - r = await handler({ - method: 'get', - path: '/post', - query: { sort: 'foo' }, - prisma, - }); - expect(r.status).toBe(400); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-sort', - }, - ], - }); - - // sort with collection - r = await handler({ - method: 'get', - path: '/post', - query: { sort: 'comments' }, - prisma, - }); - expect(r.status).toBe(400); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-sort', - }, - ], - }); - - // sort with regular field in the middle - r = await handler({ - method: 'get', - path: '/post', - query: { sort: 'viewCount.foo' }, - prisma, - }); - expect(r.status).toBe(400); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-sort', - }, - ], - }); - }); - - it('related data sorting', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { - id: 1, - title: 'Post1', - viewCount: 1, - published: true, - setting: { create: { boost: 1 } }, - }, - { - id: 2, - title: 'Post2', - viewCount: 2, - published: false, - setting: { create: { boost: 2 } }, - }, - ], - }, - }, - }); - - // asc - let r = await handler({ - method: 'get', - path: '/user/user1/posts', - query: { sort: 'viewCount' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 1 }); - - // desc - r = await handler({ - method: 'get', - path: '/user/user1/posts', - query: { sort: '-viewCount' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - // relation field - r = await handler({ - method: 'get', - path: '/user/user1/posts', - query: { sort: '-setting.boost' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - }); - - it('relationship sorting', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { - id: 1, - title: 'Post1', - viewCount: 1, - published: true, - setting: { create: { boost: 1 } }, - }, - { - id: 2, - title: 'Post2', - viewCount: 2, - published: false, - setting: { create: { boost: 2 } }, - }, - ], - }, - }, - }); - - // asc - let r = await handler({ - method: 'get', - path: '/user/user1/relationships/posts', - query: { sort: 'viewCount' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 1 }); - - // desc - r = await handler({ - method: 'get', - path: '/user/user1/relationships/posts', - query: { sort: '-viewCount' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - - // relation field - r = await handler({ - method: 'get', - path: '/user/user1/relationships/posts', - query: { sort: '-setting.boost' }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data[0]).toMatchObject({ id: 2 }); - }); - - it('including', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: { id: 1, title: 'Post1', comments: { create: { content: 'Comment1' } } }, - }, - profile: { - create: { gender: 'male' }, - }, - }, - }); - await prisma.user.create({ - data: { - myId: 'user2', - email: 'user2@abc.com', - posts: { - create: { - id: 2, - title: 'Post2', - viewCount: 1, - published: true, - comments: { create: { content: 'Comment2' } }, - }, - }, - }, - }); - - // collection query include - let r = await handler({ - method: 'get', - path: '/user', - query: { include: 'posts' }, - prisma, - }); - expect(r.body.included).toHaveLength(2); - expect(r.body.included[0]).toMatchObject({ - type: 'post', - id: 1, - attributes: { title: 'Post1' }, - }); - - // single query include - r = await handler({ - method: 'get', - path: '/user/user1', - query: { include: 'posts' }, - prisma, - }); - expect(r.body.included).toHaveLength(1); - expect(r.body.included[0]).toMatchObject({ - type: 'post', - id: 1, - attributes: { title: 'Post1' }, - }); - - // related query include - r = await handler({ - method: 'get', - path: '/user/user1/posts', - query: { include: 'posts.comments' }, - prisma, - }); - expect(r.body.included).toHaveLength(1); - expect(r.body.included[0]).toMatchObject({ - type: 'comment', - attributes: { content: 'Comment1' }, - }); - - // related query include with filter - r = await handler({ - method: 'get', - path: '/user/user1/posts', - query: { include: 'posts.comments', ['filter[published]']: 'true' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - - // deep include - r = await handler({ - method: 'get', - path: '/user', - query: { include: 'posts.comments' }, - prisma, - }); - expect(r.body.included).toHaveLength(4); - expect(r.body.included[2]).toMatchObject({ - type: 'comment', - attributes: { content: 'Comment1' }, - }); - - // multiple include - r = await handler({ - method: 'get', - path: '/user', - query: { include: 'posts.comments,profile' }, - prisma, - }); - expect(r.body.included).toHaveLength(5); - const profile = r.body.included.find((item: any) => item.type === 'profile'); - expect(profile).toMatchObject({ - type: 'profile', - attributes: { gender: 'male' }, - }); - - // invalid include - r = await handler({ - method: 'get', - path: '/user', - query: { include: 'foo' }, - prisma, - }); - expect(r.status).toBe(400); - expect(r.body).toMatchObject({ - errors: [{ status: 400, code: 'unsupported-relationship' }], - }); - }); - - it('toplevel pagination', async () => { - for (const i of Array(5).keys()) { - await prisma.user.create({ - data: { - myId: `user${i}`, - email: `user${i}@abc.com`, - }, - }); - } - - // limit only - let r = await handler({ - method: 'get', - path: '/user', - query: { ['page[limit]']: '3' }, - prisma, - }); - expect(r.body.data).toHaveLength(3); - expect(r.body.meta.total).toBe(5); - expect(r.body.links).toMatchObject({ - first: 'http://localhost/api/user?page%5Blimit%5D=3', - last: 'http://localhost/api/user?page%5Boffset%5D=3', - prev: null, - next: 'http://localhost/api/user?page%5Boffset%5D=3&page%5Blimit%5D=3', - }); - - // limit & offset - r = await handler({ - method: 'get', - path: '/user', - query: { ['page[limit]']: '3', ['page[offset]']: '3' }, - prisma, - }); - expect(r.body.data).toHaveLength(2); - expect(r.body.meta.total).toBe(5); - expect(r.body.links).toMatchObject({ - first: 'http://localhost/api/user?page%5Blimit%5D=3', - last: 'http://localhost/api/user?page%5Boffset%5D=3', - prev: 'http://localhost/api/user?page%5Boffset%5D=0&page%5Blimit%5D=3', - next: null, - }); - - // limit trimmed - r = await handler({ - method: 'get', - path: '/user', - query: { ['page[limit]']: '10' }, - prisma, - }); - expect(r.body.data).toHaveLength(5); - expect(r.body.links).toMatchObject({ - first: 'http://localhost/api/user?page%5Blimit%5D=5', - last: 'http://localhost/api/user?page%5Boffset%5D=0', - prev: null, - next: null, - }); - - // offset overflow - r = await handler({ - method: 'get', - path: '/user', - query: { ['page[offset]']: '10' }, - prisma, - }); - expect(r.body.data).toHaveLength(0); - expect(r.body.links).toMatchObject({ - first: 'http://localhost/api/user?page%5Blimit%5D=5', - last: 'http://localhost/api/user?page%5Boffset%5D=0', - prev: null, - next: null, - }); - - // minus offset - r = await handler({ - method: 'get', - path: '/user', - query: { ['page[offset]']: '-1' }, - prisma, - }); - expect(r.body.data).toHaveLength(5); - expect(r.body.links).toMatchObject({ - first: 'http://localhost/api/user?page%5Blimit%5D=5', - last: 'http://localhost/api/user?page%5Boffset%5D=0', - prev: null, - next: null, - }); - - // zero limit - r = await handler({ - method: 'get', - path: '/user', - query: { ['page[limit]']: '0' }, - prisma, - }); - expect(r.body.data).toHaveLength(5); - expect(r.body.links).toMatchObject({ - first: 'http://localhost/api/user?page%5Blimit%5D=5', - last: 'http://localhost/api/user?page%5Boffset%5D=0', - prev: null, - next: null, - }); - }); - - it('related data pagination', async () => { - await prisma.user.create({ - data: { - myId: `user1`, - email: `user1@abc.com`, - posts: { - create: [...Array(10).keys()].map((i) => ({ - id: i, - title: `Post${i}`, - })), - }, - }, - }); - - // default limiting - let r = await handler({ - method: 'get', - path: '/user/user1/posts', - prisma, - }); - expect(r.body.data).toHaveLength(5); - expect(r.body.links).toMatchObject({ - self: 'http://localhost/api/user/user1/posts', - first: 'http://localhost/api/user/user1/posts?page%5Blimit%5D=5', - last: 'http://localhost/api/user/user1/posts?page%5Boffset%5D=5', - prev: null, - next: 'http://localhost/api/user/user1/posts?page%5Boffset%5D=5&page%5Blimit%5D=5', - }); - - // explicit limiting - r = await handler({ - method: 'get', - path: '/user/user1/posts', - query: { ['page[limit]']: '3' }, - prisma, - }); - expect(r.body.data).toHaveLength(3); - expect(r.body.links).toMatchObject({ - self: 'http://localhost/api/user/user1/posts', - first: 'http://localhost/api/user/user1/posts?page%5Blimit%5D=3', - last: 'http://localhost/api/user/user1/posts?page%5Boffset%5D=9', - prev: null, - next: 'http://localhost/api/user/user1/posts?page%5Boffset%5D=3&page%5Blimit%5D=3', - }); - - // offset - r = await handler({ - method: 'get', - path: '/user/user1/posts', - query: { ['page[limit]']: '3', ['page[offset]']: '8' }, - prisma, - }); - expect(r.body.data).toHaveLength(2); - expect(r.body.links).toMatchObject({ - self: 'http://localhost/api/user/user1/posts', - first: 'http://localhost/api/user/user1/posts?page%5Blimit%5D=3', - last: 'http://localhost/api/user/user1/posts?page%5Boffset%5D=9', - prev: 'http://localhost/api/user/user1/posts?page%5Boffset%5D=5&page%5Blimit%5D=3', - next: null, - }); - }); - - it('relationship pagination', async () => { - await prisma.user.create({ - data: { - myId: `user1`, - email: `user1@abc.com`, - posts: { - create: [...Array(10).keys()].map((i) => ({ - id: i, - title: `Post${i}`, - })), - }, - }, - }); - - // default limiting - let r = await handler({ - method: 'get', - path: '/user/user1/relationships/posts', - prisma, - }); - expect(r.body.data).toHaveLength(5); - expect(r.body.links).toMatchObject({ - self: 'http://localhost/api/user/user1/relationships/posts', - first: 'http://localhost/api/user/user1/relationships/posts?page%5Blimit%5D=5', - last: 'http://localhost/api/user/user1/relationships/posts?page%5Boffset%5D=5', - prev: null, - next: 'http://localhost/api/user/user1/relationships/posts?page%5Boffset%5D=5&page%5Blimit%5D=5', - }); - - // explicit limiting - r = await handler({ - method: 'get', - path: '/user/user1/relationships/posts', - query: { ['page[limit]']: '3' }, - prisma, - }); - expect(r.body.data).toHaveLength(3); - expect(r.body.links).toMatchObject({ - self: 'http://localhost/api/user/user1/relationships/posts', - first: 'http://localhost/api/user/user1/relationships/posts?page%5Blimit%5D=3', - last: 'http://localhost/api/user/user1/relationships/posts?page%5Boffset%5D=9', - prev: null, - next: 'http://localhost/api/user/user1/relationships/posts?page%5Boffset%5D=3&page%5Blimit%5D=3', - }); - - // offset - r = await handler({ - method: 'get', - path: '/user/user1/relationships/posts', - query: { ['page[limit]']: '3', ['page[offset]']: '8' }, - prisma, - }); - expect(r.body.data).toHaveLength(2); - expect(r.body.links).toMatchObject({ - self: 'http://localhost/api/user/user1/relationships/posts', - first: 'http://localhost/api/user/user1/relationships/posts?page%5Blimit%5D=3', - last: 'http://localhost/api/user/user1/relationships/posts?page%5Boffset%5D=9', - prev: 'http://localhost/api/user/user1/relationships/posts?page%5Boffset%5D=5&page%5Blimit%5D=3', - next: null, - }); - }); - - describe('compound id', () => { - beforeEach(async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { title: 'Post1' } } }, - }); - await prisma.user.create({ - data: { myId: 'user2', email: 'user2@abc.com' }, - }); - await prisma.postLike.create({ - data: { userId: 'user2', postId: 1, superLike: false }, - }); - }); - - it('get all', async () => { - const r = await handler({ - method: 'get', - path: '/postLike', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - data: [ - { - type: 'postLike', - id: `1${idDivider}user2`, - attributes: { userId: 'user2', postId: 1, superLike: false }, - }, - ], - }); - }); - - it('get single', async () => { - const r = await handler({ - method: 'get', - path: `/postLike/1${idDivider}user2`, // Order of ids is same as in the model @@id - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - data: { - type: 'postLike', - id: `1${idDivider}user2`, - attributes: { userId: 'user2', postId: 1, superLike: false }, - }, - }); - }); - - it('get as relationship', async () => { - const r = await handler({ - method: 'get', - path: `/post/1`, - query: { include: 'likes' }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - data: { - relationships: { - likes: { - data: [{ type: 'postLike', id: `1${idDivider}user2` }], - }, - }, - }, - included: [ - expect.objectContaining({ - type: 'postLike', - id: '1_user2', - attributes: { - postId: 1, - userId: 'user2', - superLike: false, - }, - links: { - self: 'http://localhost/api/postLike/1_user2', - }, - }), - ], - }); - }); - }); - }); - - describe('POST', () => { - it('creates an item without relation', async () => { - const r = await handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { - data: { type: 'user', attributes: { myId: 'user1', email: 'user1@abc.com' } }, - }, - prisma, - }); - - expect(r.status).toBe(201); - expect(r.body).toMatchObject({ - jsonapi: { version: '1.1' }, - data: { - type: 'user', - id: 'user1', - attributes: { email: 'user1@abc.com' }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - related: 'http://localhost/api/user/user1/posts', - }, - data: [], - }, - }, - links: { self: 'http://localhost/api/user/user1' }, - }, - }); - }); - - it('creates an item with date coercion', async () => { - const r = await handler({ - method: 'post', - path: '/post', - query: {}, - requestBody: { - data: { - type: 'post', - attributes: { - id: 1, - title: 'Post1', - published: true, - publishedAt: '2024-03-02T05:00:00.000Z', - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - }); - - it('creates an item with zod violation', async () => { - const r = await handler({ - method: 'post', - path: '/post', - query: {}, - requestBody: { - data: { - type: 'post', - attributes: { - id: 1, - title: 'a very very long long title', - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(422); - expect(r.body.errors[0].code).toBe('invalid-payload'); - }); - - it('creates an item with collection relations', async () => { - await prisma.post.create({ - data: { id: 1, title: 'Post1' }, - }); - await prisma.post.create({ - data: { id: 2, title: 'Post2' }, - }); - - const r = await handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { - data: { - type: 'user', - attributes: { myId: 'user1', email: 'user1@abc.com' }, - relationships: { - posts: { - data: [ - { type: 'post', id: 1 }, - { type: 'post', id: 2 }, - ], - }, - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - expect(r.body).toMatchObject({ - jsonapi: { version: '1.1' }, - data: { - type: 'user', - id: 'user1', - attributes: { - email: 'user1@abc.com', - }, - links: { - self: 'http://localhost/api/user/user1', - }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - related: 'http://localhost/api/user/user1/posts', - }, - data: [ - { type: 'post', id: 1 }, - { type: 'post', id: 2 }, - ], - }, - }, - }, - }); - }); - - it('creates an item with single relation', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com' }, - }); - - const r = await handler({ - method: 'post', - path: '/post', - query: {}, - requestBody: { - data: { - type: 'post', - attributes: { title: 'Post1' }, - relationships: { - author: { - data: { type: 'user', id: 'user1' }, - }, - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/post/1', - }, - data: { - type: 'post', - id: 1, - attributes: { - title: 'Post1', - authorId: 'user1', - published: false, - viewCount: 0, - }, - links: { - self: 'http://localhost/api/post/1', - }, - relationships: { - author: { - links: { - self: 'http://localhost/api/post/1/relationships/author', - related: 'http://localhost/api/post/1/author', - }, - data: { type: 'user', id: 'user1' }, - }, - }, - }, - }); - }); - - it('create single relation disallowed', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ - data: { id: 1, title: 'Post1' }, - }); - - const r = await handler({ - method: 'post', - path: '/post/1/relationships/author', - query: {}, - requestBody: { - data: { type: 'user', id: 'user1' }, - }, - prisma, - }); - - expect(r.status).toBe(400); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-verb', - title: 'The HTTP verb is not supported', - }, - ], - }); - }); - - it('create a collection of relations', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ - data: { id: 1, title: 'Post1' }, - }); - await prisma.post.create({ - data: { id: 2, title: 'Post2' }, - }); - - const r = await handler({ - method: 'post', - path: '/user/user1/relationships/posts', - query: {}, - requestBody: { - data: [ - { type: 'post', id: 1 }, - { type: 'post', id: 2 }, - ], - }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - }, - data: [ - { type: 'post', id: 1 }, - { type: 'post', id: 2 }, - ], - }); - }); - - it('create relation for nonexistent entity', async () => { - let r = await handler({ - method: 'post', - path: '/user/user1/relationships/posts', - query: {}, - requestBody: { - data: [{ type: 'post', id: 1 }], - }, - prisma, - }); - - expect(r.status).toBe(404); - - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com' }, - }); - - r = await handler({ - method: 'post', - path: '/user/user1/relationships/posts', - query: {}, - requestBody: { data: [{ type: 'post', id: 1 }] }, - prisma, - }); - - expect(r.status).toBe(404); - }); - - it('create relation with compound id', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ data: { id: 1, title: 'Post1' } }); - - const r = await handler({ - method: 'post', - path: '/postLike', - query: {}, - requestBody: { - data: { - type: 'postLike', - attributes: { userId: 'user1', postId: 1, superLike: false }, - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - }); - - it('compound id create single', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ - data: { id: 1, title: 'Post1' }, - }); - - const r = await handler({ - method: 'post', - path: '/postLike', - query: {}, - requestBody: { - data: { - type: 'postLike', - id: `1${idDivider}user1`, - attributes: { userId: 'user1', postId: 1, superLike: false }, - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - }); - - it('create an entity related to an entity with compound id', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ data: { id: 1, title: 'Post1' } }); - await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } }); - - const r = await handler({ - method: 'post', - path: '/postLikeInfo', - query: {}, - requestBody: { - data: { - type: 'postLikeInfo', - attributes: { text: 'LikeInfo1' }, - relationships: { - postLike: { - data: { type: 'postLike', id: `1${idDivider}user1` }, - }, - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - }); - - it('upsert a new entity', async () => { - const r = await handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { - data: { - type: 'user', - attributes: { myId: 'user1', email: 'user1@abc.com' }, - }, - meta: { - operation: 'upsert', - matchFields: ['myId'], - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - expect(r.body).toMatchObject({ - jsonapi: { version: '1.1' }, - data: { - type: 'user', - id: 'user1', - attributes: { email: 'user1@abc.com' }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - related: 'http://localhost/api/user/user1/posts', - }, - data: [], - }, - }, - }, - }); - }); - - it('upsert an existing entity', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com' }, - }); - - const r = await handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { - data: { - type: 'user', - attributes: { myId: 'user1', email: 'user2@abc.com' }, - }, - meta: { - operation: 'upsert', - matchFields: ['myId'], - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - expect(r.body).toMatchObject({ - jsonapi: { version: '1.1' }, - data: { - type: 'user', - id: 'user1', - attributes: { email: 'user2@abc.com' }, - }, - }); - }); - - it('upsert fails if matchFields are not unique', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com' }, - }); - - const r = await handler({ - method: 'post', - path: '/profile', - query: {}, - requestBody: { - data: { - type: 'profile', - attributes: { gender: 'male' }, - relationships: { - user: { - data: { type: 'user', id: 'user1' }, - }, - }, - }, - meta: { - operation: 'upsert', - matchFields: ['gender'], - }, - }, - prisma, - }); - - expect(r.status).toBe(400); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-payload', - }, - ], - }); - }); - - it('upsert works with compound id', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ data: { id: 1, title: 'Post1' } }); - - const r = await handler({ - method: 'post', - path: '/postLike', - query: {}, - requestBody: { - data: { - type: 'postLike', - id: `1${idDivider}user1`, - attributes: { userId: 'user1', postId: 1, superLike: false }, - }, - meta: { - operation: 'upsert', - matchFields: ['userId', 'postId'], - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - }); - }); - - describe('PUT', () => { - it('updates an item if it exists', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - }, - }); - await prisma.post.create({ - data: { id: 1, title: 'Post1' }, - }); - await prisma.post.create({ - data: { id: 2, title: 'Post2' }, - }); - - const r = await handler({ - method: 'put', - path: '/user/user1', - query: {}, - requestBody: { - data: { - type: 'user', - attributes: { email: 'user2@abc.com' }, - relationships: { - posts: { - data: [ - { type: 'post', id: 1 }, - { type: 'post', id: 2 }, - ], - }, - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/user/user1', - }, - data: { - type: 'user', - id: 'user1', - attributes: { - email: 'user2@abc.com', - }, - links: { - self: 'http://localhost/api/user/user1', - }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - related: 'http://localhost/api/user/user1/posts', - }, - data: [ - { type: 'post', id: 1 }, - { type: 'post', id: 2 }, - ], - }, - }, - }, - }); - }); - - it("returns an empty data list in relationships if it's empty", async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - }, - }); - - const r = await handler({ - method: 'put', - path: '/user/user1', - query: {}, - requestBody: { - data: { - type: 'user', - attributes: { email: 'user2@abc.com' }, - }, - }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/user/user1', - }, - data: { - type: 'user', - id: 'user1', - attributes: { - email: 'user2@abc.com', - }, - links: { - self: 'http://localhost/api/user/user1', - }, - relationships: { - posts: { - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - related: 'http://localhost/api/user/user1/posts', - }, - data: [], - }, - }, - }, - }); - }); - - it('returns 404 if the user does not exist', async () => { - const r = await handler({ - method: 'put', - path: '/user/nonexistentuser', - query: {}, - requestBody: { - data: { - type: 'user', - attributes: { email: 'user2@abc.com' }, - }, - }, - prisma, - }); - - expect(r.status).toBe(404); - expect(r.body).toEqual({ - errors: [ - { - code: 'not-found', - status: 404, - title: 'Resource not found', - }, - ], - }); - }); - - it('update an item with date coercion', async () => { - await prisma.post.create({ data: { id: 1, title: 'Post1' } }); - - const r = await handler({ - method: 'put', - path: '/post/1', - query: {}, - requestBody: { - data: { - type: 'post', - attributes: { - published: true, - publishedAt: '2024-03-02T05:00:00.000Z', - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(200); - }); - - it('update an item with zod violation', async () => { - await prisma.post.create({ data: { id: 1, title: 'Post1' } }); - - const r = await handler({ - method: 'put', - path: '/post/1', - query: {}, - requestBody: { - data: { - type: 'post', - attributes: { - publishedAt: '2024-13-01', - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(422); - expect(r.body.errors[0].code).toBe('invalid-payload'); - }); - - it('update item with compound id', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ data: { id: 1, title: 'Post1' } }); - await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } }); - - const r = await handler({ - method: 'put', - path: `/postLike/1${idDivider}user1`, - query: {}, - requestBody: { - data: { - type: 'postLike', - attributes: { superLike: true }, - }, - }, - prisma, - }); - - expect(r.status).toBe(200); - }); - - it('update the id of an item with compound id', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ data: { id: 1, title: 'Post1' } }); - await prisma.post.create({ data: { id: 2, title: 'Post2' } }); - await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } }); - - const r = await handler({ - method: 'put', - path: `/postLike/1${idDivider}user1`, - query: {}, - requestBody: { - data: { - type: 'postLike', - relationships: { - post: { data: { type: 'post', id: 2 } }, - }, - }, - }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body.data.id).toBe(`2${idDivider}user1`); - }); - - it('update a single relation', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ - data: { id: 1, title: 'Post1' }, - }); - - const r = await handler({ - method: 'patch', - path: '/post/1/relationships/author', - query: {}, - requestBody: { - data: { - type: 'user', - id: 'user1', - }, - }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - jsonapi: { - version: '1.1', - }, - links: { - self: 'http://localhost/api/post/1/relationships/author', - }, - data: { - type: 'user', - id: 'user1', - }, - }); - }); - - it('remove a single relation', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { id: 1, title: 'Post1' } } }, - }); - - const r = await handler({ - method: 'patch', - path: '/post/1/relationships/author', - query: {}, - requestBody: { data: null }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/post/1/relationships/author', - }, - data: null, - }); - }); - - it('update a collection of relations', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { id: 1, title: 'Post1' } } }, - }); - await prisma.post.create({ - data: { id: 2, title: 'Post2' }, - }); - - const r = await handler({ - method: 'patch', - path: '/user/user1/relationships/posts', - query: {}, - requestBody: { - data: [{ type: 'post', id: 2 }], - }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - }, - data: [{ type: 'post', id: 2 }], - }); - }); - - it('update a collection of relations with compound id', async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ data: { id: 1, title: 'Post1' } }); - await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } }); - - const r = await handler({ - method: 'patch', - path: '/post/1/relationships/likes', - query: {}, - requestBody: { - data: [{ type: 'postLike', id: `1${idDivider}user1`, attributes: { superLike: true } }], - }, - prisma, - }); - - expect(r.status).toBe(200); - }); - - it('update a collection of relations to empty', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { id: 1, title: 'Post1' } } }, - }); - - const r = await handler({ - method: 'patch', - path: '/user/user1/relationships/posts', - query: {}, - requestBody: { data: [] }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - }, - data: [], - }); - }); - - it('update relation for nonexistent entity', async () => { - let r = await handler({ - method: 'patch', - path: '/post/1/relationships/author', - query: {}, - requestBody: { - data: { - type: 'user', - id: 'user1', - }, - }, - prisma, - }); - expect(r.status).toBe(404); - - await prisma.post.create({ - data: { id: 1, title: 'Post1' }, - }); - - r = await handler({ - method: 'patch', - path: '/post/1/relationships/author', - query: {}, - requestBody: { - data: { - type: 'user', - id: 'user1', - }, - }, - prisma, - }); - - expect(r.status).toBe(404); - }); - }); - - describe('DELETE', () => { - it('deletes an item if it exists', async () => { - // Create a user first - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - }, - }); - - const r = await handler({ - method: 'delete', - path: '/user/user1', - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ meta: {} }); - }); - - it('deletes an item with compound id', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { id: 1, title: 'Post1' } } }, - }); - await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } }); - - const r = await handler({ - method: 'delete', - path: `/postLike/1${idDivider}user1`, - prisma, - }); - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ meta: {} }); - }); - - it('returns 404 if the user does not exist', async () => { - const r = await handler({ - method: 'delete', - path: '/user/nonexistentuser', - prisma, - }); - - expect(r.status).toBe(404); - expect(r.body).toEqual({ - errors: [ - { - code: 'not-found', - status: 404, - title: 'Resource not found', - }, - ], - }); - }); - - it('delete single relation disallowed', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { id: 1, title: 'Post1' } } }, - }); - - const r = await handler({ - method: 'delete', - path: '/post/1/relationships/author', - query: {}, - prisma, - }); - - expect(r.status).toBe(400); - expect(r.body).toMatchObject({ - errors: [ - { - status: 400, - code: 'invalid-verb', - title: 'The HTTP verb is not supported', - }, - ], - }); - }); - - it('delete a collection of relations', async () => { - await prisma.user.create({ - data: { - myId: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { id: 1, title: 'Post1' }, - { id: 2, title: 'Post2' }, - ], - }, - }, - }); - - const r = await handler({ - method: 'delete', - path: '/user/user1/relationships/posts', - query: {}, - requestBody: { - data: [{ type: 'post', id: 1 }], - }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - jsonapi: { - version: '1.1', - }, - links: { - self: 'http://localhost/api/user/user1/relationships/posts', - }, - data: [{ type: 'post', id: 2 }], - }); - }); - - it('delete relations for nonexistent entity', async () => { - const r = await handler({ - method: 'delete', - path: '/user/user1/relationships/posts', - query: {}, - requestBody: { - data: [{ type: 'post', id: 1 }], - }, - prisma, - }); - expect(r.status).toBe(404); - }); - }); - - describe('validation error', () => { - it('creates an item without relation', async () => { - const r = await handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { - data: { type: 'user', attributes: { myId: 'user1', email: 'user1.com' } }, - }, - prisma, - }); - - expect(r.status).toBe(422); - expect(r.body.errors[0].code).toBe('invalid-payload'); - expect(r.body.errors[0].reason).toBe(CrudFailureReason.DATA_VALIDATION_VIOLATION); - expect(r.body.errors[0].zodErrors).toBeTruthy(); - }); - }); - }); - }); - - describe('REST server tests - enhanced prisma', () => { - const schema = ` - model Foo { - id Int @id - value Int - - @@allow('create,read', true) - @@allow('update', value > 0) - } - - model Bar { - id Int @id - value Int - - @@allow('create', true) - } - `; - - beforeAll(async () => { - const params = await loadSchema(schema); - - prisma = params.enhanceRaw(params.prisma, params); - zodSchemas = params.zodSchemas; - modelMeta = params.modelMeta; - - const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5 }); - handler = (args) => - _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - }); - - it('update policy rejection test', async () => { - let r = await handler({ - method: 'post', - path: '/foo', - query: {}, - requestBody: { - data: { type: 'foo', attributes: { id: 1, value: 0 } }, - }, - prisma, - }); - expect(r.status).toBe(201); - - r = await handler({ - method: 'put', - path: '/foo/1', - query: {}, - requestBody: { - data: { type: 'foo', attributes: { value: 1 } }, - }, - prisma, - }); - expect(r.status).toBe(403); - expect(r.body.errors[0].code).toBe('forbidden'); - expect(r.body.errors[0].reason).toBe(CrudFailureReason.ACCESS_POLICY_VIOLATION); - }); - - it('read-back policy rejection test', async () => { - const r = await handler({ - method: 'post', - path: '/bar', - query: {}, - requestBody: { - data: { type: 'bar', attributes: { id: 1, value: 0 } }, - }, - prisma, - }); - expect(r.status).toBe(403); - expect(r.body.errors[0].reason).toBe(CrudFailureReason.RESULT_NOT_READABLE); - }); - }); - - describe('REST server tests - NextAuth project regression', () => { - const schema = ` - model Post { - id String @id @default(cuid()) - title String - content String - - // full access for all - @@allow('all', true) - } - - model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? - access_token String? - expires_at Int? - token_type String? - scope String? - id_token String? - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) - } - - model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - } - - model User { - id String @id @default(cuid()) - name String? - email String @email @unique - password String @password @omit - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - - @@allow('create,read', true) - @@allow('delete,update', auth() != null) - } - - model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) - } - `; - - beforeAll(async () => { - const params = await loadSchema(schema); - - prisma = params.enhanceRaw(params.prisma, params); - zodSchemas = params.zodSchemas; - modelMeta = params.modelMeta; - - const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5 }); - handler = (args) => - _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - }); - - it('crud test', async () => { - let r = await handler({ - method: 'get', - path: '/user', - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(0); - - r = await handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { - data: { type: 'user', attributes: { email: 'user1@abc.com', password: '1234' } }, - }, - prisma, - }); - expect(r.status).toBe(201); - - r = await handler({ - method: 'get', - path: '/user', - prisma, - }); - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(1); - - r = await handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { - data: { type: 'user', attributes: { email: 'user1@abc.com', password: '1234' } }, - }, - prisma, - }); - expect(r.status).toBe(400); - expect(r.body.errors[0].prismaCode).toBe('P2002'); - }); - }); - - describe('REST server tests - field type coverage', () => { - const schema = ` - model Foo { - id Int @id - string String - int Int - bigInt BigInt - date DateTime - float Float - decimal Decimal - boolean Boolean - bytes Bytes - bars Bar[] - } - - model Bar { - id Int @id - bytes Bytes - foo Foo? @relation(fields: [fooId], references: [id]) - fooId Int? @unique - } - `; - - it('field types', async () => { - const { prisma, zodSchemas, modelMeta } = await loadSchema(schema); - - const _handler = makeHandler({ endpoint: 'http://localhost/api', pageSize: 5 }); - handler = (args) => - _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - - await prisma.bar.create({ data: { id: 1, bytes: Buffer.from([7, 8, 9]) } }); - - const decimalValue1 = new Decimal('0.046875'); - const decimalValue2 = new Decimal('0.0146875'); - - const createAttrs = { - string: 'string', - int: 123, - bigInt: BigInt(534543543534), - date: new Date(), - float: 1.23, - decimal: decimalValue1, - boolean: true, - bytes: new Uint8Array([1, 2, 3, 4]), - }; - - const { json: createPayload, meta: createMeta } = SuperJSON.serialize({ - data: { - type: 'foo', - attributes: { id: 1, ...createAttrs }, - relationships: { - bars: { - data: [{ type: 'bar', id: 1 }], - }, - }, - }, - }); - - let r = await handler({ - method: 'post', - path: '/foo', - query: {}, - requestBody: { - ...(createPayload as any), - meta: { - serialization: createMeta, - }, - }, - prisma, - }); - expect(r.status).toBe(201); - // result is serializable - expect(JSON.stringify(r.body)).toBeTruthy(); - let serializationMeta = r.body.meta.serialization; - expect(serializationMeta).toBeTruthy(); - let deserialized: any = SuperJSON.deserialize({ json: r.body, meta: serializationMeta }); - let data = deserialized.data.attributes; - expect(typeof data.bigInt).toBe('bigint'); - expect(data.bytes).toBeInstanceOf(Uint8Array); - expect(data.date instanceof Date).toBeTruthy(); - expect(Decimal.isDecimal(data.decimal)).toBeTruthy(); - - const updateAttrs = { - bigInt: BigInt(1534543543534), - date: new Date(), - decimal: decimalValue2, - bytes: new Uint8Array([5, 2, 3, 4]), - }; - const { json: updatePayload, meta: updateMeta } = SuperJSON.serialize({ - data: { - type: 'foo', - attributes: updateAttrs, - }, - }); - - r = await handler({ - method: 'put', - path: '/foo/1', - query: {}, - requestBody: { - ...(updatePayload as any), - meta: { - serialization: updateMeta, - }, - }, - prisma, - }); - expect(r.status).toBe(200); - // result is serializable - expect(JSON.stringify(r.body)).toBeTruthy(); - - serializationMeta = r.body.meta.serialization; - expect(serializationMeta).toBeTruthy(); - deserialized = SuperJSON.deserialize({ json: r.body, meta: serializationMeta }); - data = deserialized.data.attributes; - expect(data.bigInt).toEqual(updateAttrs.bigInt); - expect(data.date).toEqual(updateAttrs.date); - expect(data.decimal.equals(updateAttrs.decimal)).toBeTruthy(); - expect(data.bytes.toString()).toEqual(updateAttrs.bytes.toString()); - - r = await handler({ - method: 'get', - path: '/foo/1', - query: {}, - prisma, - }); - // result is serializable - expect(JSON.stringify(r.body)).toBeTruthy(); - serializationMeta = r.body.meta.serialization; - expect(serializationMeta).toBeTruthy(); - deserialized = SuperJSON.deserialize({ json: r.body, meta: serializationMeta }); - data = deserialized.data.attributes; - expect(typeof data.bigInt).toBe('bigint'); - expect(data.bytes).toBeInstanceOf(Uint8Array); - expect(data.date instanceof Date).toBeTruthy(); - expect(Decimal.isDecimal(data.decimal)).toBeTruthy(); - - r = await handler({ - method: 'get', - path: '/foo', - query: { include: 'bars' }, - prisma, - }); - // result is serializable - expect(JSON.stringify(r.body)).toBeTruthy(); - serializationMeta = r.body.meta.serialization; - expect(serializationMeta).toBeTruthy(); - deserialized = SuperJSON.deserialize({ json: r.body, meta: serializationMeta }); - const included = deserialized.included[0]; - expect(included.attributes.bytes).toBeInstanceOf(Uint8Array); - }); - }); - - describe('REST server tests - compound id with custom separator', () => { - const schema = ` - enum Role { - COMMON_USER - ADMIN_USER - } - - model User { - email String - role Role - enabled Boolean @default(true) - - @@id([email, role]) - } - `; - const idDivider = ':'; - const dbName = 'restful-compound-id-custom-separator'; - - beforeAll(async () => { - const params = await loadSchema(schema, { - provider: 'postgresql', - dbUrl: await createPostgresDb(dbName), - }); - - prisma = params.prisma; - zodSchemas = params.zodSchemas; - modelMeta = params.modelMeta; - - const _handler = makeHandler({ - endpoint: 'http://localhost/api', - pageSize: 5, - idDivider, - urlSegmentCharset: 'a-zA-Z0-9-_~ %@.:', - }); - handler = (args) => - _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - }); - - afterAll(async () => { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb(dbName); - }); - - it('POST', async () => { - const r = await handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { - data: { - type: 'user', - attributes: { email: 'user1@abc.com', role: 'COMMON_USER' }, - }, - }, - prisma, - }); - - expect(r.status).toBe(201); - }); - - it('GET', async () => { - await prisma.user.create({ - data: { email: 'user1@abc.com', role: 'COMMON_USER' }, - }); - - const r = await handler({ - method: 'get', - path: '/user', - query: {}, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body.data).toHaveLength(1); - }); - - it('GET single', async () => { - await prisma.user.create({ - data: { email: 'user1@abc.com', role: 'COMMON_USER' }, - }); - - const r = await handler({ - method: 'get', - path: '/user/user1@abc.com:COMMON_USER', - query: {}, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body.data.attributes.email).toBe('user1@abc.com'); - }); - - it('PUT', async () => { - await prisma.user.create({ - data: { email: 'user1@abc.com', role: 'COMMON_USER' }, - }); - - const r = await handler({ - method: 'put', - path: '/user/user1@abc.com:COMMON_USER', - query: {}, - requestBody: { - data: { - type: 'user', - attributes: { enabled: false }, - }, - }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body.data.attributes.enabled).toBe(false); - }); - }); - - describe('REST server tests - model name mapping', () => { - const schema = ` - model User { - id String @id @default(cuid()) - name String - posts Post[] - } - - model Post { - id String @id @default(cuid()) - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - } - `; - beforeAll(async () => { - const params = await loadSchema(schema); - prisma = params.prisma; - zodSchemas = params.zodSchemas; - modelMeta = params.modelMeta; - - const _handler = makeHandler({ - endpoint: 'http://localhost/api', - modelNameMapping: { - User: 'myUser', - }, - }); - handler = (args) => - _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - }); - - it('works with name mapping', async () => { - // using original model name - await expect( - handler({ - method: 'post', - path: '/user', - query: {}, - requestBody: { data: { type: 'user', attributes: { id: '1', name: 'User1' } } }, - prisma, - }) - ).resolves.toMatchObject({ - status: 400, - }); - - // using mapped model name - await expect( - handler({ - method: 'post', - path: '/myUser', - query: {}, - requestBody: { data: { type: 'user', attributes: { id: '1', name: 'User1' } } }, - prisma, - }) - ).resolves.toMatchObject({ - status: 201, - body: { - links: { self: 'http://localhost/api/myUser/1' }, - }, - }); - - await expect( - handler({ - method: 'get', - path: '/myUser/1', - query: {}, - prisma, - }) - ).resolves.toMatchObject({ - status: 200, - body: { - links: { self: 'http://localhost/api/myUser/1' }, - }, - }); - - // works with unmapped model name - await expect( - handler({ - method: 'post', - path: '/post', - query: {}, - requestBody: { - data: { - type: 'post', - attributes: { id: '1', title: 'Post1' }, - relationships: { - author: { data: { type: 'user', id: '1' } }, - }, - }, - }, - prisma, - }) - ).resolves.toMatchObject({ - status: 201, - }); - }); - }); - - describe('REST server tests - external id mapping', () => { - const schema = ` - model User { - id Int @id @default(autoincrement()) - name String - source String - posts Post[] - - @@unique([name, source]) - } - - model Post { - id Int @id @default(autoincrement()) - title String - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - } - `; - beforeAll(async () => { - const params = await loadSchema(schema); - prisma = params.prisma; - zodSchemas = params.zodSchemas; - modelMeta = params.modelMeta; - - const _handler = makeHandler({ - endpoint: 'http://localhost/api', - externalIdMapping: { - User: 'name_source', - }, - }); - handler = (args) => - _handler({ ...args, zodSchemas, modelMeta, url: new URL(`http://localhost/${args.path}`) }); - }); - - it('works with id mapping', async () => { - await prisma.user.create({ - data: { id: 1, name: 'User1', source: 'a' }, - }); - - // user is no longer exposed using the `id` field - let r = await handler({ - method: 'get', - path: '/user/1', - query: {}, - prisma, - }); - - expect(r.status).toBe(400); - - // user is exposed using the fields from the `name__source` multi-column unique index - r = await handler({ - method: 'get', - path: '/user/User1_a', - query: {}, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body.data.attributes.source).toBe('a'); - expect(r.body.data.attributes.name).toBe('User1'); - - await prisma.post.create({ - data: { id: 1, title: 'Title1', authorId: 1 }, - }); - - // post is exposed using the `id` field - r = await handler({ - method: 'get', - path: '/post/1', - query: { include: 'author' }, - prisma, - }); - - expect(r.status).toBe(200); - expect(r.body.data.attributes.title).toBe('Title1'); - // Verify author relationship contains the external ID - expect(r.body.data.relationships.author.data).toMatchObject({ - type: 'user', - id: 'User1_a', - }); - }); - }); -}); diff --git a/packages/server/tests/api/rpc.test.ts b/packages/server/tests/api/rpc.test.ts deleted file mode 100644 index 416ba4a6c..000000000 --- a/packages/server/tests/api/rpc.test.ts +++ /dev/null @@ -1,575 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/// - -import { CrudFailureReason, type ZodSchemas } from '@zenstackhq/runtime'; -import { loadSchema } from '@zenstackhq/testtools'; -import { Decimal } from 'decimal.js'; -import SuperJSON from 'superjson'; -import { RPCApiHandler } from '../../src/api'; -import { schema } from '../utils'; - -describe('RPC API Handler Tests', () => { - let prisma: any; - let enhance: any; - let modelMeta: any; - let zodSchemas: any; - - beforeAll(async () => { - const params = await loadSchema(schema, { fullZod: true, generatePermissionChecker: true }); - prisma = params.prisma; - enhance = params.enhance; - modelMeta = params.modelMeta; - zodSchemas = params.zodSchemas; - }); - - it('crud', async () => { - const handleRequest = makeHandler(); - - let r = await handleRequest({ - method: 'get', - path: '/post/findMany', - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(0); - - r = await handleRequest({ - method: 'post', - path: '/user/create', - query: {}, - requestBody: { - include: { posts: true }, - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { title: 'post1', published: true, viewCount: 1 }, - { title: 'post2', published: false, viewCount: 2 }, - ], - }, - }, - }, - prisma, - }); - expect(r.status).toBe(201); - expect(r.data).toEqual( - expect.objectContaining({ - email: 'user1@abc.com', - posts: expect.arrayContaining([ - expect.objectContaining({ title: 'post1' }), - expect.objectContaining({ title: 'post2' }), - ]), - }) - ); - - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(2); - - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ where: { viewCount: { gt: 1 } } }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(1); - - r = await handleRequest({ - method: 'put', - path: '/user/update', - requestBody: { where: { id: 'user1' }, data: { email: 'user1@def.com' } }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data.email).toBe('user1@def.com'); - - r = await handleRequest({ - method: 'get', - path: '/post/count', - query: { q: JSON.stringify({ where: { viewCount: { gt: 1 } } }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toBe(1); - - r = await handleRequest({ - method: 'get', - path: '/post/aggregate', - query: { q: JSON.stringify({ _sum: { viewCount: true } }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data._sum.viewCount).toBe(3); - - r = await handleRequest({ - method: 'get', - path: '/post/groupBy', - query: { q: JSON.stringify({ by: ['published'], _sum: { viewCount: true } }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toEqual( - expect.arrayContaining([ - expect.objectContaining({ published: true, _sum: { viewCount: 1 } }), - expect.objectContaining({ published: false, _sum: { viewCount: 2 } }), - ]) - ); - - r = await handleRequest({ - method: 'delete', - path: '/user/deleteMany', - query: { q: JSON.stringify({ where: { id: 'user1' } }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data.count).toBe(1); - }); - - it('pagination and ordering', async () => { - const handleRequest = makeHandler(); - - // Clean up any existing data first - await prisma.post.deleteMany(); - await prisma.user.deleteMany(); - - // Create test data - await prisma.user.create({ - data: { - id: 'user1', - email: 'user1@abc.com', - posts: { - create: [ - { id: '1', title: 'A Post', published: true, viewCount: 5 }, - { id: '2', title: 'B Post', published: true, viewCount: 3 }, - { id: '3', title: 'C Post', published: true, viewCount: 7 }, - { id: '4', title: 'D Post', published: true, viewCount: 1 }, - { id: '5', title: 'E Post', published: true, viewCount: 9 }, - ], - }, - }, - }); - - // Test orderBy with title ascending - let r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ orderBy: { title: 'asc' } }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(5); - expect(r.data[0].title).toBe('A Post'); - expect(r.data[4].title).toBe('E Post'); - - // Test orderBy with viewCount descending - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ orderBy: { viewCount: 'desc' } }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data[0].viewCount).toBe(9); - expect(r.data[4].viewCount).toBe(1); - - // Test multiple orderBy - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ orderBy: [{ published: 'desc' }, { title: 'asc' }] }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data[0].title).toBe('A Post'); - - // Test take (limit) - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ take: 3 }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(3); - - // Test skip (offset) - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ skip: 2, take: 2 }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(2); - - // Test skip and take with orderBy - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ orderBy: { title: 'asc' }, skip: 1, take: 3 }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(3); - expect(r.data[0].title).toBe('B Post'); - expect(r.data[2].title).toBe('D Post'); - - // Test cursor-based pagination - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ orderBy: { id: 'asc' }, take: 2 }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(2); - const lastId = r.data[1].id; - - // Get next page using cursor - r = await handleRequest({ - method: 'get', - path: '/post/findMany', - query: { q: JSON.stringify({ orderBy: { id: 'asc' }, take: 2, skip: 1, cursor: { id: lastId } }) }, - prisma, - }); - expect(r.status).toBe(200); - expect(r.data).toHaveLength(2); - expect(r.data[0].id).toBe('3'); - expect(r.data[1].id).toBe('4'); - - // Clean up - await prisma.post.deleteMany(); - await prisma.user.deleteMany(); - }); - - it('check', async () => { - const handleRequest = makeHandler(); - - let r = await handleRequest({ - method: 'get', - path: '/post/check', - query: { q: JSON.stringify({ operation: 'read' }) }, - prisma: enhance(), - }); - expect(r.status).toBe(200); - expect(r.data).toEqual(true); - - r = await handleRequest({ - method: 'get', - path: '/post/check', - query: { q: JSON.stringify({ operation: 'read', where: { published: false } }) }, - prisma: enhance(), - }); - expect(r.status).toBe(200); - expect(r.data).toEqual(false); - - r = await handleRequest({ - method: 'get', - path: '/post/check', - query: { q: JSON.stringify({ operation: 'read', where: { authorId: '1', published: false } }) }, - prisma: enhance({ id: '1' }), - }); - expect(r.status).toBe(200); - expect(r.data).toEqual(true); - }); - - it('policy violation', async () => { - // Clean up any existing data first - await prisma.post.deleteMany(); - await prisma.user.deleteMany(); - - await prisma.user.create({ - data: { - id: '1', - email: 'user1@abc.com', - posts: { create: { id: '1', title: 'post1', published: true } }, - }, - }); - - const handleRequest = makeHandler(); - - const r = await handleRequest({ - method: 'put', - path: '/post/update', - requestBody: { - where: { id: '1' }, - data: { title: 'post2' }, - }, - prisma: enhance(), - }); - expect(r.status).toBe(403); - expect(r.error.rejectedByPolicy).toBeTruthy(); - expect(r.error.reason).toBe(CrudFailureReason.ACCESS_POLICY_VIOLATION); - }); - - it('validation error', async () => { - let handleRequest = makeHandler(); - - // without validation - let r = await handleRequest({ - method: 'get', - path: '/post/findUnique', - prisma, - }); - expect(r.status).toBe(400); - expect(r.error.message).toMatch(/Argument.+missing/); - - handleRequest = makeHandler(zodSchemas); - - // with validation - r = await handleRequest({ - method: 'get', - path: '/post/findUnique', - prisma, - }); - expect(r.status).toBe(422); - expect(r.error.message).toContain('Validation error'); - expect(r.error.message).toContain('where'); - - r = await handleRequest({ - method: 'post', - path: '/post/create', - requestBody: { data: {} }, - prisma, - zodSchemas, - }); - expect(r.status).toBe(422); - expect(r.error.message).toContain('Validation error'); - expect(r.error.message).toContain('data'); - - r = await handleRequest({ - method: 'post', - path: '/user/create', - requestBody: { data: { email: 'hello' } }, - prisma: enhance(), - zodSchemas, - }); - expect(r.status).toBe(422); - expect(r.error.message).toContain('Validation error'); - expect(r.error.message).toContain('email'); - }); - - it('invalid path or args', async () => { - const handleRequest = makeHandler(); - let r = await handleRequest({ - method: 'get', - path: '/post/', - prisma, - }); - expect(r.status).toBe(400); - expect(r.error.message).toContain('invalid request path'); - - r = await handleRequest({ - method: 'get', - path: '/post/findMany/abc', - prisma, - }); - expect(r.status).toBe(400); - expect(r.error.message).toContain('invalid request path'); - - r = await handleRequest({ - method: 'get', - path: '/post/findUnique', - query: { q: 'abc' }, - prisma, - }); - expect(r.status).toBe(400); - expect(r.error.message).toContain('invalid "q" query parameter'); - - r = await handleRequest({ - method: 'delete', - path: '/post/deleteMany', - query: { q: 'abc' }, - prisma, - }); - expect(r.status).toBe(400); - expect(r.error.message).toContain('invalid "q" query parameter'); - }); - - it('field types', async () => { - const schema = ` - model Foo { - id Int @id - - string String - int Int - bigInt BigInt - date DateTime - float Float - decimal Decimal - boolean Boolean - bytes Bytes - bars Bar[] - - @@allow('all', true) - } - - - model Bar { - id Int @id @default(autoincrement()) - bytes Bytes - foo Foo @relation(fields: [fooId], references: [id]) - fooId Int @unique - } - `; - - const handleRequest = makeHandler(); - const { prisma, zodSchemas, modelMeta } = await loadSchema(schema); - - const decimalValue = new Decimal('0.046875'); - const bigIntValue = BigInt(534543543534); - const dateValue = new Date(); - const bytesValue = new Uint8Array([1, 2, 3, 4]); - const barBytesValue = new Uint8Array([7, 8, 9]); - - const createData = { - string: 'string', - int: 123, - bigInt: bigIntValue, - date: dateValue, - float: 1.23, - decimal: decimalValue, - boolean: true, - bytes: bytesValue, - bars: { - create: { bytes: barBytesValue }, - }, - }; - - const serialized = SuperJSON.serialize({ - include: { bars: true }, - data: { id: 1, ...createData }, - }); - - let r = await handleRequest({ - method: 'post', - path: '/foo/create', - query: {}, - prisma, - zodSchemas, - modelMeta, - requestBody: { - ...(serialized.json as any), - meta: { serialization: serialized.meta }, - }, - }); - expect(r.status).toBe(201); - expect(r.meta).toBeTruthy(); - const data: any = SuperJSON.deserialize({ json: r.data, meta: r.meta.serialization }); - expect(typeof data.bigInt).toBe('bigint'); - expect(data.bytes).toBeInstanceOf(Uint8Array); - expect(data.date instanceof Date).toBeTruthy(); - expect(Decimal.isDecimal(data.decimal)).toBeTruthy(); - expect(data.bars[0].bytes).toBeInstanceOf(Uint8Array); - - // find with filter not found - const serializedQ = SuperJSON.serialize({ - where: { - bigInt: { - gt: bigIntValue, - }, - }, - }); - r = await handleRequest({ - method: 'get', - path: '/foo/findFirst', - query: { - q: JSON.stringify(serializedQ.json), - meta: JSON.stringify({ serialization: serializedQ.meta }), - }, - prisma, - zodSchemas, - modelMeta, - }); - expect(r.status).toBe(200); - expect(r.data).toBeNull(); - - // find with filter found - const serializedQ1 = SuperJSON.serialize({ - where: { - bigInt: bigIntValue, - }, - }); - r = await handleRequest({ - method: 'get', - path: '/foo/findFirst', - query: { - q: JSON.stringify(serializedQ1.json), - meta: JSON.stringify({ serialization: serializedQ1.meta }), - }, - prisma, - zodSchemas, - modelMeta, - }); - expect(r.status).toBe(200); - expect(r.data).toBeTruthy(); - - // find with filter found - const serializedQ2 = SuperJSON.serialize({ - where: { - bars: { - some: { - bytes: barBytesValue, - }, - }, - }, - }); - r = await handleRequest({ - method: 'get', - path: '/foo/findFirst', - query: { - q: JSON.stringify(serializedQ2.json), - meta: JSON.stringify({ serialization: serializedQ2.meta }), - }, - prisma, - zodSchemas, - modelMeta, - }); - expect(r.status).toBe(200); - expect(r.data).toBeTruthy(); - - // find with filter not found - const serializedQ3 = SuperJSON.serialize({ - where: { - bars: { - none: { - bytes: barBytesValue, - }, - }, - }, - }); - r = await handleRequest({ - method: 'get', - path: '/foo/findFirst', - query: { - q: JSON.stringify(serializedQ3.json), - meta: JSON.stringify({ serialization: serializedQ3.meta }), - }, - prisma, - zodSchemas, - modelMeta, - }); - expect(r.status).toBe(200); - expect(r.data).toBeNull(); - }); - - function makeHandler(zodSchemas?: ZodSchemas) { - const _handler = RPCApiHandler(); - return async (args: any) => { - const r = await _handler({ ...args, url: new URL(`http://localhost/${args.path}`), modelMeta, zodSchemas }); - return { - status: r.status, - body: r.body as any, - data: (r.body as any).data, - error: (r.body as any).error, - meta: (r.body as any).meta, - }; - }; - } -}); diff --git a/packages/server/tests/utils.ts b/packages/server/tests/utils.ts deleted file mode 100644 index 78cc38345..000000000 --- a/packages/server/tests/utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import superjson from 'superjson'; - -export const schema = ` -model User { - id String @id @default(cuid()) - createdAt DateTime @default (now()) - updatedAt DateTime @updatedAt - email String @unique @email - posts Post[] - - @@allow('all', auth() == this) - @@allow('create,read', true) -} - -model Post { - id String @id @default(cuid()) - createdAt DateTime @default (now()) - updatedAt DateTime @updatedAt - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - publishedAt DateTime? - viewCount Int @default(0) - - @@allow('all', author == auth()) - @@allow('read', published) -} -`; - -export function makeUrl(path: string, q?: object, useSuperJson = false) { - return q ? `${path}?q=${encodeURIComponent(useSuperJson ? superjson.stringify(q) : JSON.stringify(q))}` : path; -} diff --git a/packages/testtools/CHANGELOG.md b/packages/testtools/CHANGELOG.md deleted file mode 100644 index cc2a59fdc..000000000 --- a/packages/testtools/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changelog - -## [2.0.0-alpha.2](https://github.com/zenstackhq/zenstack/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2024-02-21) - - -### Miscellaneous Chores - -* release 2.0.0-alpha.2 ([f40d7e3](https://github.com/zenstackhq/zenstack/commit/f40d7e3718d4210137a2e131d28b5491d065b914)) diff --git a/packages/testtools/LICENSE b/packages/testtools/LICENSE deleted file mode 120000 index 30cff7403..000000000 --- a/packages/testtools/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/testtools/README.md b/packages/testtools/README.md deleted file mode 100644 index 40164a988..000000000 --- a/packages/testtools/README.md +++ /dev/null @@ -1 +0,0 @@ -# ZenStack Test Tools diff --git a/packages/testtools/src/db.ts b/packages/testtools/src/db.ts deleted file mode 100644 index 774400a95..000000000 --- a/packages/testtools/src/db.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Pool } from 'pg'; - -const USERNAME = process.env.ZENSTACK_TEST_DB_USER || 'postgres'; -const PASSWORD = process.env.ZENSTACK_TEST_DB_PASS || 'abc123'; -const CONNECTION_DB = process.env.ZENSTACK_TEST_DB_NAME || 'postgres'; -const HOST = process.env.ZENSTACK_TEST_DB_HOST || 'localhost'; -const PORT = (process.env.ZENSTACK_TEST_DB_PORT ? parseInt(process.env.ZENSTACK_TEST_DB_PORT) : null) || 5432; - -function connect() { - return new Pool({ - user: USERNAME, - password: PASSWORD, - database: CONNECTION_DB, - host: HOST, - port: PORT - }); -} - -export async function createPostgresDb(db: string) { - const pool = connect(); - await pool.query(`DROP DATABASE IF EXISTS "${db}";`); - await pool.query(`CREATE DATABASE "${db}";`); - await pool.end(); - return `postgresql://${USERNAME}:${PASSWORD}@${HOST}:${PORT}/${db}`; -} - -export async function dropPostgresDb(db: string) { - const pool = connect(); - await pool.query(`DROP DATABASE IF EXISTS "${db}";`); - await pool.end(); -} diff --git a/packages/testtools/src/jest-ext.ts b/packages/testtools/src/jest-ext.ts deleted file mode 100644 index 244ba40f3..000000000 --- a/packages/testtools/src/jest-ext.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { format } from 'util'; -import { isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; - -function isPromise(value: any) { - return typeof value.then === 'function' && typeof value.catch === 'function'; -} - -export const toBeRejectedByPolicy = async function (received: Promise, expectedMessages?: string[]) { - if (!isPromise(received)) { - return { message: () => 'a promise is expected', pass: false }; - } - try { - await received; - } catch (err: any) { - if (expectedMessages) { - const message = err.message || ''; - for (const m of expectedMessages) { - if (!message.includes(m)) { - return { - message: () => `expected message not found in error: ${m}, got message: ${message}`, - pass: false, - }; - } - } - } - return expectPrismaCode(err, 'P2004'); - } - return { - message: () => `expected PrismaClientKnownRequestError, got no error`, - pass: false, - }; -}; - -export const toBeNotFound = async function (received: Promise) { - if (!isPromise(received)) { - return { message: () => 'a promise is expected', pass: false }; - } - try { - await received; - } catch (err) { - return expectPrismaCode(err, 'P2025'); - } - return { - message: () => `expected PrismaClientKnownRequestError, got no error`, - pass: false, - }; -}; - -export const toBeRejectedWithCode = async function (received: Promise, code: string) { - if (!isPromise(received)) { - return { message: () => 'a promise is expected', pass: false }; - } - try { - await received; - } catch (err) { - return expectPrismaCode(err, code); - } - return { - message: () => `expected PrismaClientKnownRequestError, got no error`, - pass: false, - }; -}; - -export const toResolveTruthy = async function (received: Promise) { - if (!isPromise(received)) { - return { message: () => 'a promise is expected', pass: false }; - } - try { - const r = await received; - if (r) { - return { - message: () => '', - pass: true, - }; - } else { - return { - message: () => 'resolved to a falsy value', - pass: false, - }; - } - } catch (err) { - return { - message: () => `promise rejected: ${err}`, - pass: false, - }; - } -}; - -export const toResolveFalsy = async function (received: Promise) { - if (!isPromise(received)) { - return { message: () => 'a promise is expected', pass: false }; - } - try { - const r = await received; - if (!r) { - return { - message: () => '', - pass: true, - }; - } else { - return { - message: () => `resolved to a truthy value: ${r}`, - pass: false, - }; - } - } catch (err) { - return { - message: () => `promise rejected: ${err}`, - pass: false, - }; - } -}; - -export const toResolveNull = async function (received: Promise) { - if (!isPromise(received)) { - return { message: () => 'a promise is expected', pass: false }; - } - try { - const r = await received; - if (r === null) { - return { - message: () => '', - pass: true, - }; - } else { - return { - message: () => `resolved to a non-null value: ${format(r)}`, - pass: false, - }; - } - } catch (err) { - return { - message: () => `promise rejected: ${err}`, - pass: false, - }; - } -}; - -function expectPrismaCode(err: any, code: string) { - if (!isPrismaClientKnownRequestError(err)) { - return { - message: () => `expected PrismaClientKnownRequestError, got ${err}`, - pass: false, - }; - } - const errCode = err.code; - if (errCode !== code) { - return { - message: () => `expected PrismaClientKnownRequestError.code '${code}', got '${errCode ?? err}'`, - pass: false, - }; - } - return { - message: () => '', - pass: true, - }; -} diff --git a/packages/testtools/src/model.ts b/packages/testtools/src/model.ts deleted file mode 100644 index b6e2a3b71..000000000 --- a/packages/testtools/src/model.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Model } from '@zenstackhq/sdk/ast'; -import * as fs from 'fs'; -import { NodeFileSystem } from 'langium/node'; -import * as path from 'path'; -import * as tmp from 'tmp'; -import { URI } from 'vscode-uri'; -import { createZModelServices } from 'zenstack/language-server/zmodel-module'; -import { mergeBaseModels } from 'zenstack/utils/ast-utils'; - -tmp.setGracefulCleanup(); - -export class SchemaLoadingError extends Error { - constructor(public readonly errors: string[]) { - super('Schema error:\n' + errors.join('\n')); - } -} - -export async function loadModel(content: string, validate = true, verbose = true) { - const { name: docPath } = tmp.fileSync({ postfix: '.zmodel' }); - fs.writeFileSync(docPath, content); - const { shared, ZModel } = createZModelServices(NodeFileSystem); - const stdLib = shared.workspace.LangiumDocuments.getOrCreateDocument( - URI.file(path.resolve(__dirname, '../../schema/src/res/stdlib.zmodel')) - ); - const doc = shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(docPath)); - - if (doc.parseResult.lexerErrors.length > 0) { - throw new SchemaLoadingError(doc.parseResult.lexerErrors.map((e) => e.message)); - } - - if (doc.parseResult.parserErrors.length > 0) { - throw new SchemaLoadingError(doc.parseResult.parserErrors.map((e) => e.message)); - } - - await shared.workspace.DocumentBuilder.build([stdLib, doc], { - validationChecks: validate ? 'all' : 'none', - }); - - const validationErrors = (doc.diagnostics ?? []).filter((e) => e.severity === 1); - if (validationErrors.length > 0) { - for (const validationError of validationErrors) { - if (verbose) { - const range = doc.textDocument.getText(validationError.range); - console.error( - `line ${validationError.range.start.line + 1}: ${validationError.message}${ - range ? ' [' + range + ']' : '' - }` - ); - } - } - throw new SchemaLoadingError(validationErrors.map((e) => e.message)); - } - - const model = (await doc.parseResult.value) as Model; - - mergeBaseModels(model, ZModel.references.Linker); - - return model; -} - -export async function loadModelWithError(content: string, verbose = false) { - try { - await loadModel(content, true, verbose); - } catch (err) { - if (!(err instanceof SchemaLoadingError)) { - throw err; - } - return (err as SchemaLoadingError).errors; - } - throw new Error('No error is thrown'); -} diff --git a/samples/next.js/zenstack/models.ts b/samples/next.js/zenstack/models.ts index 3314c7d48..d878eac47 100644 --- a/samples/next.js/zenstack/models.ts +++ b/samples/next.js/zenstack/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema-lite"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; /** * User model */ diff --git a/samples/nuxt/zenstack/models.ts b/samples/nuxt/zenstack/models.ts index 3314c7d48..d878eac47 100644 --- a/samples/nuxt/zenstack/models.ts +++ b/samples/nuxt/zenstack/models.ts @@ -6,7 +6,7 @@ /* eslint-disable */ import { type SchemaType as $Schema } from "./schema-lite"; -import { type ModelResult as $ModelResult } from "@zenstackhq/orm"; +import type { ModelResult as $ModelResult } from "@zenstackhq/orm"; /** * User model */ diff --git a/script/set-test-env.ts b/script/set-test-env.ts deleted file mode 100644 index 4db61d8d1..000000000 --- a/script/set-test-env.ts +++ /dev/null @@ -1 +0,0 @@ -process.env.ZENSTACK_TEST = '1'; diff --git a/script/test-global-setup.ts b/script/test-global-setup.ts deleted file mode 100644 index 514cccae7..000000000 --- a/script/test-global-setup.ts +++ /dev/null @@ -1,9 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -export default function globalSetup() { - if (!fs.existsSync(path.join(__dirname, '../.test/scaffold/package-lock.json'))) { - console.error(`Test scaffold not found. Please run \`pnpm test-scaffold\` first.`); - process.exit(1); - } -} diff --git a/script/test-scaffold.ts b/script/test-scaffold.ts deleted file mode 100644 index b9c4aae4d..000000000 --- a/script/test-scaffold.ts +++ /dev/null @@ -1,26 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import { execSync } from 'child_process'; - -const scaffoldPath = path.join(__dirname, '../.test/scaffold'); -if (fs.existsSync(scaffoldPath)) { - fs.rmSync(scaffoldPath, { recursive: true, force: true }); -} -fs.mkdirSync(scaffoldPath, { recursive: true }); - -function run(cmd: string) { - console.log(`Running: ${cmd}, in ${scaffoldPath}`); - try { - execSync(cmd, { cwd: scaffoldPath, stdio: 'ignore' }); - } catch (err) { - console.error(`Test project scaffolding cmd error: ${err}`); - throw err; - } -} - -run('npm init -y'); -run( - 'npm i --no-audit --no-fund typescript@~5.8.0 prisma@6.11.x @prisma/client@6.11.x zod@^3.25.0 decimal.js @types/node' -); - -console.log('Test scaffold setup complete.'); diff --git a/test-setup.ts b/test-setup.ts deleted file mode 100644 index 9856ff4b5..000000000 --- a/test-setup.ts +++ /dev/null @@ -1,9 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -export default function globalSetup() { - if (!fs.existsSync(path.join(__dirname, '.test/scaffold/package-lock.json'))) { - console.error(`Test scaffold not found. Please run \`pnpm test-scaffold\` first.`); - process.exit(1); - } -} diff --git a/tests/integration/.eslintrc.json b/tests/integration/.eslintrc.json deleted file mode 100644 index 24ebad85a..000000000 --- a/tests/integration/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "extends": ["plugin:jest/recommended"], - "rules": { - "jest/expect-expect": "off" - } -} diff --git a/tests/integration/.gitignore b/tests/integration/.gitignore deleted file mode 100644 index 01f5e1303..000000000 --- a/tests/integration/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.npmcache -node_modules -test-run/cases -tests/**/test-run diff --git a/tests/integration/jest.config.ts b/tests/integration/jest.config.ts deleted file mode 100644 index 67a118269..000000000 --- a/tests/integration/jest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import baseConfig from '../../jest.config'; - -/* - * For a detailed explanation regarding each configuration property and type check, visit: - * https://jestjs.io/docs/configuration - */ -export default { - ...baseConfig, - setupFilesAfterEnv: ['./test-setup.ts'], -}; diff --git a/tests/integration/jest.d.ts b/tests/integration/jest.d.ts deleted file mode 100644 index ff066029a..000000000 --- a/tests/integration/jest.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -interface CustomMatchers { - toBeRejectedByPolicy(expectedMessages?: string[]): Promise; - toBeNotFound(): Promise; - toResolveTruthy(): Promise; - toResolveFalsy(): Promise; - toResolveNull(): Promise; - toBeRejectedWithCode(code: string): Promise; -} -declare global { - namespace jest { - interface Expect extends CustomMatchers {} - interface Matchers extends CustomMatchers {} - interface InverseAsymmetricMatchers extends CustomMatchers {} - } -} -export {}; diff --git a/tests/integration/package.json b/tests/integration/package.json deleted file mode 100644 index dcd7355ad..000000000 --- a/tests/integration/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "integration", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "lint": "eslint . --ext .ts", - "test": "jest" - }, - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "@types/bcryptjs": "^2.4.2", - "@types/fs-extra": "^11.0.1", - "@types/pg": "^8.10.2", - "@types/supertest": "^2.0.12", - "@types/tmp": "^0.2.3", - "@types/uuid": "^8.3.4", - "@zenstackhq/runtime": "workspace:*", - "@zenstackhq/server": "workspace:*", - "@zenstackhq/swr": "workspace:*", - "@zenstackhq/trpc": "workspace:*", - "fs-extra": "^11.1.0", - "next": "14.2.4", - "tmp": "^0.2.1", - "uuid": "^10.0.0", - "zenstack": "workspace: *" - }, - "dependencies": { - "@zenstackhq/sdk": "workspace:*", - "@zenstackhq/testtools": "workspace:*", - "bcryptjs": "^2.4.3", - "decimal.js": "^10.4.2", - "pg": "^8.11.1", - "superjson": "^1.13.0" - } -} diff --git a/tests/integration/test-run/.gitignore b/tests/integration/test-run/.gitignore deleted file mode 100644 index 483a9c42c..000000000 --- a/tests/integration/test-run/.gitignore +++ /dev/null @@ -1 +0,0 @@ -package-lock.json \ No newline at end of file diff --git a/tests/integration/test-run/package.json b/tests/integration/test-run/package.json deleted file mode 100644 index 8cfe59b2f..000000000 --- a/tests/integration/test-run/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "test-run", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@prisma/client": "6.19.x", - "@zenstackhq/runtime": "file:../../../packages/runtime/dist", - "prisma": "6.19.x", - "react": "^18.2.0", - "swr": "^1.3.0", - "zenstack": "file:../../../packages/schema/dist", - "zod": "^3.25.0" - } -} diff --git a/tests/integration/test-setup.ts b/tests/integration/test-setup.ts deleted file mode 100644 index b6147c287..000000000 --- a/tests/integration/test-setup.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - toBeRejectedByPolicy, - toBeNotFound, - toResolveTruthy, - toResolveFalsy, - toResolveNull, - toBeRejectedWithCode, -} from '@zenstackhq/testtools/jest-ext'; - -expect.extend({ - toBeRejectedByPolicy, - toBeNotFound, - toResolveTruthy, - toResolveFalsy, - toResolveNull, - toBeRejectedWithCode, -}); diff --git a/tests/integration/tests/cli/format.test.ts b/tests/integration/tests/cli/format.test.ts deleted file mode 100644 index 723e69f0b..000000000 --- a/tests/integration/tests/cli/format.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as fs from 'fs'; -import * as tmp from 'tmp'; -import { createProgram } from '../../../../packages/schema/src/cli'; - -tmp.setGracefulCleanup(); - -describe('CLI format test', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - const r = tmp.dirSync({ unsafeCleanup: true }); - console.log(`Project dir: ${r.name}`); - process.chdir(r.name); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('basic format', async () => { - const model = ` - datasource db {provider="sqlite" url="file:./dev.db"} - generator client {provider = "prisma-client-js"} - model Post {id Int @id() @default(autoincrement())users User[]foo Unsupported("foo")}`; - - const formattedModel = ` -datasource db { - provider="sqlite" - url="file:./dev.db" -} -generator client { - provider = "prisma-client-js" -} -model Post { - id Int @id() @default(autoincrement()) - users User[] - foo Unsupported("foo") -}`; - // set up schema - fs.writeFileSync('schema.zmodel', model, 'utf-8'); - const program = createProgram(); - await program.parseAsync(['format', '--no-prisma-style'], { from: 'user' }); - - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(formattedModel); - }); - - it('prisma format', async () => { - const model = ` - datasource db {provider="sqlite" url="file:./dev.db"} - generator client {provider = "prisma-client-js"} - model Post {id Int @id() @default(autoincrement())users User[]foo Unsupported("foo")}`; - - const formattedModel = ` -datasource db { - provider="sqlite" - url="file:./dev.db" -} -generator client { - provider = "prisma-client-js" -} -model Post { - id Int @id() @default(autoincrement()) - users User[] - foo Unsupported("foo") -}`; - // set up schema - fs.writeFileSync('schema.zmodel', model, 'utf-8'); - const program = createProgram(); - await program.parseAsync(['format'], { from: 'user' }); - - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(formattedModel); - }); -}); diff --git a/tests/integration/tests/cli/generate.test.ts b/tests/integration/tests/cli/generate.test.ts deleted file mode 100644 index a420b30e8..000000000 --- a/tests/integration/tests/cli/generate.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/// - -import { installPackage } from '@zenstackhq/testtools'; -import * as fs from 'fs'; -import path from 'path'; -import * as tmp from 'tmp'; -import { createProgram } from '../../../../packages/schema/src/cli'; -import { createNpmrc } from './share'; - -tmp.setGracefulCleanup(); - -describe('CLI generate command tests', () => { - let origDir: string; - const MODEL = ` -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -generator js { - provider = "prisma-client-js" -} - -model User { - id Int @id @default(autoincrement()) - email String @unique @email - posts Post[] -} - -model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int -} - `; - - beforeEach(() => { - origDir = process.cwd(); - const r = tmp.dirSync({ unsafeCleanup: true }); - console.log(`Project dir: ${r.name}`); - process.chdir(r.name); - - // set up project - fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); - createNpmrc(); - installPackage('prisma@6.19.x @prisma/client@6.19.x zod@^3.25.0 @types/node@20'); - installPackage(path.join(__dirname, '../../../../packages/runtime/dist')); - - // set up schema - fs.writeFileSync('schema.zmodel', MODEL, 'utf-8'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('generate standard', async () => { - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check'], { from: 'user' }); - expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeTruthy(); - expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeTruthy(); - expect(fs.existsSync('./node_modules/.zenstack/zod/index.js')).toBeTruthy(); - }); - - it('generate custom output default', async () => { - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check', '-o', 'out'], { from: 'user' }); - expect(fs.existsSync('./node_modules/.zenstack')).toBeFalsy(); - expect(fs.existsSync('./out/policy.ts')).toBeTruthy(); - expect(fs.existsSync('./out/model-meta.ts')).toBeTruthy(); - expect(fs.existsSync('./out/policy.js')).toBeFalsy(); - expect(fs.existsSync('./out/model-meta.js')).toBeFalsy(); - expect(fs.existsSync('./out/zod')).toBeTruthy(); - }); - - it('generate custom output non-std schema location', async () => { - fs.mkdirSync('./schema'); - fs.cpSync('schema.zmodel', './schema/my.zmodel'); - fs.rmSync('schema.zmodel'); - - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check', '--schema', './schema/my.zmodel', '-o', 'out'], { - from: 'user', - }); - expect(fs.existsSync('./node_modules/.zenstack')).toBeFalsy(); - expect(fs.existsSync('./out/policy.ts')).toBeTruthy(); - expect(fs.existsSync('./out/model-meta.ts')).toBeTruthy(); - expect(fs.existsSync('./out/policy.js')).toBeFalsy(); - expect(fs.existsSync('./out/model-meta.js')).toBeFalsy(); - expect(fs.existsSync('./out/zod')).toBeTruthy(); - }); - - it('generate no default plugins run nothing', async () => { - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); - expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeFalsy(); - expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeFalsy(); - expect(fs.existsSync('./prisma/schema.prisma')).toBeFalsy(); - }); - - it('generate no default plugins with prisma only', async () => { - fs.appendFileSync( - 'schema.zmodel', - ` - plugin prisma { - provider = '@core/prisma' - } - ` - ); - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); - expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeFalsy(); - expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeFalsy(); - expect(fs.existsSync('./prisma/schema.prisma')).toBeTruthy(); - }); - - it('generate no default plugins with enhancer and zod', async () => { - fs.appendFileSync( - 'schema.zmodel', - ` - plugin prisma { - provider = '@core/prisma' - } - - plugin zod { - provider = '@core/zod' - } - - plugin enhancer { - provider = '@core/enhancer' - } - ` - ); - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); - expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeTruthy(); - expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeTruthy(); - expect(fs.existsSync('./prisma/schema.prisma')).toBeTruthy(); - }); - - it('generate no compile', async () => { - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check', '--no-compile'], { from: 'user' }); - expect(fs.existsSync('./node_modules/.zenstack/policy.js')).toBeFalsy(); - expect(fs.existsSync('./node_modules/.zenstack/policy.ts')).toBeTruthy(); - expect(fs.existsSync('./node_modules/.zenstack/model-meta.js')).toBeFalsy(); - expect(fs.existsSync('./node_modules/.zenstack/model-meta.ts')).toBeTruthy(); - expect(fs.existsSync('./node_modules/.zenstack/zod/index.js')).toBeFalsy(); - expect(fs.existsSync('./node_modules/.zenstack/zod/index.ts')).toBeTruthy(); - }); - - it('generate with prisma generateArgs', async () => { - fs.appendFileSync( - 'schema.zmodel', - ` - plugin prisma { - provider = '@core/prisma' - generateArgs = '--no-engine' - } - ` - ); - const program = createProgram(); - await program.parseAsync(['generate', '--no-dependency-check', '--no-default-plugins'], { from: 'user' }); - }); -}); diff --git a/tests/integration/tests/cli/init.test.ts b/tests/integration/tests/cli/init.test.ts deleted file mode 100644 index 892bad97b..000000000 --- a/tests/integration/tests/cli/init.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/// - -import { getWorkspaceNpmCacheFolder } from '@zenstackhq/testtools'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as tmp from 'tmp'; -import { createProgram } from '../../../../packages/schema/src/cli'; -import { execSync } from '../../../../packages/schema/src/utils/exec-utils'; -import { createNpmrc } from './share'; - -tmp.setGracefulCleanup(); - -// Skipping these tests as they seem to cause hangs intermittently when running with other tests -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('CLI init command tests', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - const r = tmp.dirSync({ unsafeCleanup: true }); - console.log(`Project dir: ${r.name}`); - process.chdir(r.name); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - // eslint-disable-next-line jest/no-disabled-tests - it('init project t3 npm std', async () => { - execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { - stdio: 'inherit', - env: { - npm_config_user_agent: 'npm', - npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), - }, - }); - createNpmrc(); - - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8')); - - checkDependency('zenstack', true, true); - checkDependency('@zenstackhq/runtime', false, true); - }); - - it('init project t3 yarn std', async () => { - execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { - stdio: 'inherit', - env: { - npm_config_user_agent: 'yarn', - npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), - }, - }); - createNpmrc(); - - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8')); - - checkDependency('zenstack', true, true); - checkDependency('@zenstackhq/runtime', false, true); - }); - - it('init project t3 pnpm std', async () => { - execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { - stdio: 'inherit', - env: { - npm_config_user_agent: 'pnpm', - npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), - }, - }); - createNpmrc(); - - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8')); - - checkDependency('zenstack', true, true); - checkDependency('@zenstackhq/runtime', false, true); - }); - - it('init project t3 non-std prisma schema', async () => { - execSync('npx --yes create-t3-app@latest --prisma --CI --noGit .', { - stdio: 'inherit', - env: { - npm_config_user_agent: 'npm', - npm_config_cache: getWorkspaceNpmCacheFolder(__dirname), - }, - }); - createNpmrc(); - fs.renameSync('prisma/schema.prisma', 'prisma/my.prisma'); - - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest', '--prisma', 'prisma/my.prisma'], { from: 'user' }); - - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/my.prisma', 'utf-8')); - }); - - it('init project empty project', async () => { - fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); - createNpmrc(); - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toBeTruthy(); - }); - - it('init project no version check', async () => { - fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); - createNpmrc(); - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest', '--no-version-check'], { from: 'user' }); - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toBeTruthy(); - }); - - it('init project existing zmodel', async () => { - fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); - const origZModelContent = ` - datasource db { - provider = 'sqlite' - url = 'file:./todo.db' - } - `; - fs.writeFileSync('schema.zmodel', origZModelContent); - createNpmrc(); - const program = createProgram(); - await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); - expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(origZModelContent); - }); -}); - -function checkDependency(pkg: string, isDev: boolean, requireExactVersion = true) { - const pkgJson = require(path.resolve('./package.json')); - - if (isDev) { - expect(pkgJson.devDependencies[pkg]).toBeTruthy(); - if (requireExactVersion) { - expect(pkgJson.devDependencies[pkg]).not.toMatch(/^[\^~].*/); - } - } else { - expect(pkgJson.dependencies[pkg]).toBeTruthy(); - if (requireExactVersion) { - expect(pkgJson.dependencies[pkg]).not.toMatch(/^[\^~].*/); - } - } -} diff --git a/tests/integration/tests/cli/plugins.test.ts b/tests/integration/tests/cli/plugins.test.ts deleted file mode 100644 index cf37f3d32..000000000 --- a/tests/integration/tests/cli/plugins.test.ts +++ /dev/null @@ -1,313 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/// - -import { getWorkspaceNpmCacheFolder, installPackage, run } from '@zenstackhq/testtools'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as tmp from 'tmp'; -import { createProgram } from '../../../../packages/schema/src/cli'; - -tmp.setGracefulCleanup(); - -describe('CLI Plugins Tests', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - function createNpmrc() { - fs.writeFileSync('.npmrc', `cache=${getWorkspaceNpmCacheFolder(__dirname)}`); - } - - const PACKAGE_MANAGERS = ['npm' /*, 'pnpm', 'pnpm-workspace'*/] as const; - - function zenstackGenerate(pm: (typeof PACKAGE_MANAGERS)[number], output?: string) { - switch (pm) { - case 'npm': - run(`ZENSTACK_TEST=0 npx zenstack generate${output ? ' --output ' + output : ''}`); - break; - // case 'pnpm': - // case 'pnpm-workspace': - // run(`ZENSTACK_TEST=0 pnpm zenstack generate`); - // break; - } - } - - async function initProject(pm: (typeof PACKAGE_MANAGERS)[number] = 'npm') { - const r = tmp.dirSync({ unsafeCleanup: true }); - console.log(`Project dir: ${r.name}`); - process.chdir(r.name); - - createNpmrc(); - - // init project - switch (pm) { - case 'npm': - run('npm init -y'); - break; - // case 'yarn': - // run('yarn init'); - // break; - // case 'pnpm': - // case 'pnpm-workspace': - // run('pnpm init'); - // break; - } - - // if (pm === 'pnpm-workspace') { - // // create a package - // fs.writeFileSync('pnpm-workspace.yaml', JSON.stringify({ packages: ['db'] })); - // fs.mkdirSync('db'); - // process.chdir('db'); - // run('pnpm init'); - // } - - // deps - const ver = require('../../../../packages/schema/package.json').version; - const depPkgs = [ - 'zod@^3.25.0', - 'react', - 'swr', - '@tanstack/react-query@5.56.x', - '@trpc/server@10', - '@prisma/client@6.19.x', - `${path.join(__dirname, '../../../../.build/zenstackhq-language-' + ver + '.tgz')}`, - `${path.join(__dirname, '../../../../.build/zenstackhq-sdk-' + ver + '.tgz')}`, - `${path.join(__dirname, '../../../../.build/zenstackhq-runtime-' + ver + '.tgz')}`, - ]; - const deps = depPkgs.join(' '); - - const devDepPkgs = [ - 'typescript', - '@types/react', - 'prisma@6.19.x', - '@types/node@20', - `${path.join(__dirname, '../../../../.build/zenstack-' + ver + '.tgz')}`, - `${path.join(__dirname, '../../../../.build/zenstackhq-tanstack-query-' + ver + '.tgz')}`, - `${path.join(__dirname, '../../../../.build/zenstackhq-swr-' + ver + '.tgz')}`, - `${path.join(__dirname, '../../../../.build/zenstackhq-trpc-' + ver + '.tgz')}`, - `${path.join(__dirname, '../../../../.build/zenstackhq-openapi-' + ver + '.tgz')}`, - ]; - const devDeps = devDepPkgs.join(' '); - - switch (pm) { - case 'npm': - installPackage(deps); - installPackage(devDeps, true); - break; - // case 'yarn': - // run('yarn add ' + deps); - // run('yarn add --dev ' + devDeps); - // break; - // case 'pnpm': - // case 'pnpm-workspace': - // run('pnpm add ' + deps); - // run('pnpm add -D ' + devDeps); - // break; - } - - // init typescript project - fs.writeFileSync( - 'tsconfig.json', - JSON.stringify({ - compilerOptions: { - strict: true, - lib: ['esnext', 'dom'], - esModuleInterop: true, - skipLibCheck: true, - }, - }) - ); - - return createProgram(); - } - - const plugins = [ - `plugin prisma { - provider = '@core/prisma' - output = 'prisma/my.prisma' - generateClient = true - }`, - `plugin enhancer { - provider = '@core/enhancer' - }`, - `plugin tanstack { - provider = '@zenstackhq/tanstack-query' - output = 'lib/tanstack-query' - target = 'react' - }`, - `plugin swr { - provider = '@zenstackhq/swr' - output = 'lib/swr' - }`, - `plugin trpc { - provider = '@zenstackhq/trpc' - output = 'lib/trpc' - }`, - `plugin openapi { - provider = '@zenstackhq/openapi' - output = 'myapi.yaml' - specVersion = '3.0.0' - title = 'My Awesome API' - version = '1.0.0' - description = 'awesome api' - prefix = '/myapi' - securitySchemes = { - myBasic: { type: 'http', scheme: 'basic' }, - myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, - myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' } - } - }`, - ]; - - const BASE_MODEL = ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - enum role { - USER - ADMIN - } - - model User { - id String @id @default(cuid()) - email String @unique @email - role role @default(USER) - posts post_item[] - @@allow('create', true) - @@allow('all', auth() == this || role == ADMIN) - } - - model post_item { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - published Boolean @default(false) - author User? @relation(fields: [authorId], references: [id]) - authorId String? - - @@allow('read', auth() != null && published) - @@allow('all', auth() == author) - } - `; - - it('all plugins standard prisma client output path', async () => { - for (const pm of PACKAGE_MANAGERS) { - console.log('[PACKAGE MANAGER]', pm); - await initProject(pm); - - let schemaContent = ` -generator client { - provider = "prisma-client-js" -} - -${BASE_MODEL} - `; - for (const plugin of plugins) { - schemaContent += `\n${plugin}`; - } - fs.writeFileSync('schema.zmodel', schemaContent); - - // generate - zenstackGenerate(pm); - - // compile - run('npx tsc'); - } - }); - - it('all plugins custom prisma client output path', async () => { - for (const pm of PACKAGE_MANAGERS) { - console.log('[PACKAGE MANAGER]', pm); - await initProject(pm); - - let schemaContent = ` -generator client { - provider = "prisma-client-js" - output = "foo/bar" -} - -${BASE_MODEL} -`; - for (const plugin of plugins) { - schemaContent += `\n${plugin}`; - } - fs.writeFileSync('schema.zmodel', schemaContent); - - // generate - zenstackGenerate(pm); - - // compile - run('npx tsc'); - } - }); - - it('all plugins absolute prisma client output path', async () => { - for (const pm of PACKAGE_MANAGERS) { - console.log('[PACKAGE MANAGER]', pm); - const { name: output } = tmp.dirSync({ unsafeCleanup: true }); - console.log('Output prisma client to:', output); - - await initProject(pm); - - let schemaContent = ` -generator client { - provider = "prisma-client-js" - output = "${output}" -} - -${BASE_MODEL} -`; - for (const plugin of plugins) { - schemaContent += `\n${plugin}`; - } - fs.writeFileSync('schema.zmodel', schemaContent); - - // generate - zenstackGenerate(pm); - - // compile - run('npx tsc'); - } - }); - - it('all plugins custom core output path', async () => { - for (const pm of PACKAGE_MANAGERS) { - console.log('[PACKAGE MANAGER]', pm); - await initProject(pm); - - let schemaContent = ` -generator client { - provider = "prisma-client-js" -} - -${BASE_MODEL} - `; - for (const plugin of plugins) { - if (!plugin.includes('trp')) { - schemaContent += `\n${plugin}`; - } - } - - schemaContent += `plugin trpc { - provider = '@zenstackhq/trpc' - output = 'lib/trpc' - zodSchemasImport = '../../../zen/zod' - }`; - - fs.writeFileSync('schema.zmodel', schemaContent); - - // generate - zenstackGenerate(pm, './zen'); - - // compile - run('npx tsc'); - } - }); -}); diff --git a/tests/integration/tests/cli/share.ts b/tests/integration/tests/cli/share.ts deleted file mode 100644 index 7d4f8805d..000000000 --- a/tests/integration/tests/cli/share.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getWorkspaceNpmCacheFolder } from '@zenstackhq/testtools'; -import fs from 'fs'; - -export function createNpmrc() { - fs.writeFileSync('.npmrc', `cache=${getWorkspaceNpmCacheFolder(__dirname)}`); -} diff --git a/tests/integration/tests/e2e/filter-function-coverage.test.ts b/tests/integration/tests/e2e/filter-function-coverage.test.ts deleted file mode 100644 index 04724578b..000000000 --- a/tests/integration/tests/e2e/filter-function-coverage.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Filter Function Coverage Tests', () => { - it('contains case-sensitive field', async () => { - const { enhance } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - string String - @@allow('all', contains(string, 'a')) - } - ` - ); - - await expect(enhance().foo.create({ data: { string: 'bcd' } })).toBeRejectedByPolicy(); - await expect(enhance().foo.create({ data: { string: 'bac' } })).toResolveTruthy(); - }); - - it('contains case-sensitive non-field', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - name String - } - - model Foo { - id String @id @default(cuid()) - @@allow('all', contains(auth().name, 'a')) - } - ` - ); - - await expect(enhance().foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ id: 'user1', name: 'bcd' }).foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ id: 'user1', name: 'bac' }).foo.create({ data: {} })).toResolveTruthy(); - }); - - it('contains with auth()', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - name String - } - - model Foo { - id String @id @default(cuid()) - string String - @@allow('all', contains(string, auth().name)) - } - ` - ); - - await expect(enhance().foo.create({ data: { string: 'abc' } })).toBeRejectedByPolicy(); - const db = enhance({ id: '1', name: 'a' }); - await expect(db.foo.create({ data: { string: 'bcd' } })).toBeRejectedByPolicy(); - await expect(db.foo.create({ data: { string: 'bac' } })).toResolveTruthy(); - }); - - it('startsWith field', async () => { - const { enhance } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - string String - @@allow('all', startsWith(string, 'a')) - } - ` - ); - - await expect(enhance().foo.create({ data: { string: 'bac' } })).toBeRejectedByPolicy(); - await expect(enhance().foo.create({ data: { string: 'abc' } })).toResolveTruthy(); - }); - - it('startsWith non-field', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - name String - } - - model Foo { - id String @id @default(cuid()) - @@allow('all', startsWith(auth().name, 'a')) - } - ` - ); - - await expect(enhance().foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ id: 'user1', name: 'bac' }).foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ id: 'user1', name: 'abc' }).foo.create({ data: {} })).toResolveTruthy(); - }); - - it('endsWith field', async () => { - const { enhance } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - string String - @@allow('all', endsWith(string, 'a')) - } - ` - ); - - await expect(enhance().foo.create({ data: { string: 'bac' } })).toBeRejectedByPolicy(); - await expect(enhance().foo.create({ data: { string: 'bca' } })).toResolveTruthy(); - }); - - it('endsWith non-field', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - name String - } - - model Foo { - id String @id @default(cuid()) - @@allow('all', endsWith(auth().name, 'a')) - } - ` - ); - - await expect(enhance().foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ id: 'user1', name: 'bac' }).foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ id: 'user1', name: 'bca' }).foo.create({ data: {} })).toResolveTruthy(); - }); - - it('in left field', async () => { - const { enhance } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - string String - @@allow('all', string in ['a', 'b']) - } - ` - ); - - await expect(enhance().foo.create({ data: { string: 'c' } })).toBeRejectedByPolicy(); - await expect(enhance().foo.create({ data: { string: 'b' } })).toResolveTruthy(); - }); - - it('in non-field', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - name String - } - - model Foo { - id String @id @default(cuid()) - @@allow('all', auth().name in ['abc', 'bcd']) - } - ` - ); - - await expect(enhance().foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ id: 'user1', name: 'abd' }).foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ id: 'user1', name: 'abc' }).foo.create({ data: {} })).toResolveTruthy(); - }); -}); diff --git a/tests/integration/tests/e2e/misc-function-coverage.test.ts b/tests/integration/tests/e2e/misc-function-coverage.test.ts deleted file mode 100644 index 096a8dc21..000000000 --- a/tests/integration/tests/e2e/misc-function-coverage.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import { validate, version } from 'uuid'; - -describe('Misc Function Coverage Tests', () => { - it('now() function', async () => { - const { enhance } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - dt DateTime @default(now()) - @@allow('create,read', true) - @@allow('update', now() >= dt && future().dt > now()) - } - ` - ); - - const db = enhance(); - const now = new Date(); - - await db.foo.create({ data: { id: '1', dt: new Date(now.getTime() + 1000) } }); - // violates `dt <= now()` - await expect(db.foo.update({ where: { id: '1' }, data: { dt: now } })).toBeRejectedByPolicy(); - - await db.foo.create({ data: { id: '2', dt: now } }); - // violates `future().dt > now()` - await expect(db.foo.update({ where: { id: '2' }, data: { dt: now } })).toBeRejectedByPolicy(); - - // success - await expect( - db.foo.update({ where: { id: '2' }, data: { dt: new Date(now.getTime() + 10000) } }) - ).toResolveTruthy(); - expect(await db.foo.findUnique({ where: { id: '2' } })).toMatchObject({ dt: new Date(now.getTime() + 10000) }); - }); - - it('uuid() function', async () => { - const { enhance } = await loadSchema( - ` - model Foo { - id String @id @default(uuid()) - id4 String @default(uuid(4)) - id7 String @default(uuid(7)) - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - const foo = await db.foo.create({ data: {} }); - expect(validate(foo.id)).toBeTruthy(); - expect(version(foo.id)).toBe(4); - expect(validate(foo.id4)).toBeTruthy(); - expect(version(foo.id4)).toBe(4); - expect(validate(foo.id7)).toBeTruthy(); - expect(version(foo.id7)).toBe(7); - }); -}); diff --git a/tests/integration/tests/e2e/prisma-methods.test.ts b/tests/integration/tests/e2e/prisma-methods.test.ts deleted file mode 100644 index 2053f0a73..000000000 --- a/tests/integration/tests/e2e/prisma-methods.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { AuthUser } from '@zenstackhq/runtime'; -import { FullDbClientContract, loadSchema, run } from '@zenstackhq/testtools'; - -describe('Prisma Methods Tests', () => { - let getDb: (user?: AuthUser) => FullDbClientContract; - let prisma: FullDbClientContract; - - beforeAll(async () => { - const { enhance, prisma: _prisma } = await loadSchema( - ` - model Model { - id String @id @default(cuid()) - value Int - - @@allow('all', value > 0) - } - ` - ); - getDb = enhance; - prisma = _prisma; - }); - - beforeEach(() => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - }); - - it('transaction', async () => { - const db = getDb({ id: 'user1' }); - - const r = await db.$transaction(async (tx) => { - const r1 = await tx.model.create({ data: { id: '1', value: 1 } }); - const r2 = await tx.model.create({ data: { id: '2', value: 2 } }); - return [r1, r2]; - }); - - expect(r).toEqual( - expect.arrayContaining([expect.objectContaining({ id: '1' }), expect.objectContaining({ id: '2' })]) - ); - - await expect( - db.$transaction(async (tx) => { - const r3 = await tx.model.create({ data: { id: '3', value: 3 } }); - const r4 = await tx.model.create({ data: { id: '4', value: -1 } }); - return [r3, r4]; - }) - ).rejects.toThrow(); - - const r3 = await db.model.findUnique({ where: { id: '3' } }); - expect(r3).toBeNull(); - const r4 = await db.model.findUnique({ where: { id: '4' } }); - expect(r4).toBeNull(); - }); - - it('raw sql', async () => { - const db = getDb(); - - const r = await db.$transaction(async (tx) => { - const r1 = await tx.model.create({ data: { id: '1', value: 1 } }); - const r2 = await tx.model.create({ data: { id: '2', value: 2 } }); - return [r1, r2]; - }); - - const q = await db.$queryRaw`SELECT * FROM "Model" WHERE id=${r[1].id};`; - expect(q[0]).toEqual(expect.objectContaining(r[1])); - - const r1 = await db.$executeRaw`UPDATE "Model" SET value=5 WHERE id="1";`; - expect(r1).toBe(1); - await expect(db.model.findUnique({ where: { id: '1' } })).resolves.toEqual( - expect.objectContaining({ value: 5 }) - ); - }); - - it('middleware', async () => { - const db = getDb(); - let middlewareCalled = false; - db.$use(async (params: any, next: Function) => { - middlewareCalled = true; - return next(params); - }); - - await db.model.create({ data: { id: '1', value: 1 } }); - expect(middlewareCalled).toBeTruthy(); - }); - - it('extension', async () => { - const db = getDb(); - - const extendedDb: any = db.$extends({ - client: { - $hello: (name: string) => `Hello ${name}`, - }, - query: { - model: { - findMany({ query, args }: { query: Function; args: any }) { - args.where = { value: { gt: 1 }, ...args.where }; - return query(args); - }, - }, - }, - model: { - model: { - greet: (name: string) => `Greeting from ${name}`, - }, - }, - result: { - model: { - valuePlus: { - needs: { value: true }, - compute(model: any) { - return model.value + 1; - }, - }, - }, - }, - }); - - expect(extendedDb.$hello('world')).toBe('Hello world'); - - await db.model.create({ data: { id: '1', value: 1 } }); - await db.model.create({ data: { id: '2', value: 2 } }); - await expect(db.model.findMany()).resolves.toHaveLength(2); - - const r = await extendedDb.model.findMany(); - expect(r).toHaveLength(1); - expect(r[0].value).toBe(2); - expect(r[0].valuePlus).toBe(3); - - expect(extendedDb.model.greet('ymc9')).toBe('Greeting from ymc9'); - }); -}); diff --git a/tests/integration/tests/e2e/todo-presets.test.ts b/tests/integration/tests/e2e/todo-presets.test.ts deleted file mode 100644 index dbd7f4003..000000000 --- a/tests/integration/tests/e2e/todo-presets.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { AuthUser } from '@zenstackhq/runtime'; -import { loadSchemaFromFile, run, type FullDbClientContract } from '@zenstackhq/testtools'; -import { compareSync } from 'bcryptjs'; -import path from 'path'; - -describe('Todo Presets Tests', () => { - let getDb: (user?: AuthUser) => FullDbClientContract; - let prisma: FullDbClientContract; - - beforeAll(async () => { - const { enhance, prisma: _prisma } = await loadSchemaFromFile(path.join(__dirname, '../schema/todo.zmodel'), { - addPrelude: false, - }); - getDb = enhance; - prisma = _prisma; - }); - - beforeEach(() => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - }); - - it('user', async () => { - const anonDb = getDb(); - const user1Db = getDb({ id: 'user1' }); - - await expect(anonDb.user.create({ data: { email: 'abc.xyz' } })).toBeRejectedByPolicy(['Invalid email']); - - const r = await user1Db.user.create({ data: { id: 'user1', email: 'abc@xyz.com', password: 'abc123' } }); - expect(r.password).toBeUndefined(); - const full = await prisma.user.findUnique({ where: { id: 'user1' } }); - expect(compareSync('abc123', full.password)).toBe(true); - - await expect(anonDb.user.findUnique({ where: { id: 'user1' } })).toResolveNull(); - }); -}); diff --git a/tests/integration/tests/e2e/type-coverage.test.ts b/tests/integration/tests/e2e/type-coverage.test.ts deleted file mode 100644 index 659f36788..000000000 --- a/tests/integration/tests/e2e/type-coverage.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { AuthUser } from '@zenstackhq/runtime'; -import { loadSchema, run, type FullDbClientContract } from '@zenstackhq/testtools'; -import Decimal from 'decimal.js'; -import superjson from 'superjson'; - -describe('Type Coverage Tests', () => { - let getDb: (user?: AuthUser) => FullDbClientContract; - let prisma: FullDbClientContract; - - beforeAll(async () => { - const { enhance, prisma: _prisma } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - - String String - Int Int - BigInt BigInt - DateTime DateTime - Float Float - Decimal Decimal - Boolean Boolean - Bytes Bytes - - @@allow('all', true) - } - ` - ); - getDb = enhance; - prisma = _prisma; - }); - - beforeEach(() => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - }); - - it('coverage', async () => { - const db = getDb(); - - const date = new Date(); - const data = { - id: '1', - String: 'string', - Int: 100, - BigInt: BigInt(9007199254740991), - DateTime: date, - Float: 1.23, - Decimal: new Decimal(1.2345), - Boolean: true, - Bytes: new Uint8Array([1, 2, 3, 4]), - }; - - await db.foo.create({ - data, - }); - - const r = await db.foo.findUnique({ where: { id: '1' } }); - expect(superjson.stringify(r)).toEqual(superjson.stringify(data)); - }); -}); diff --git a/tests/integration/tests/enhancements/json/crud.test.ts b/tests/integration/tests/enhancements/json/crud.test.ts deleted file mode 100644 index 1a41b1b43..000000000 --- a/tests/integration/tests/enhancements/json/crud.test.ts +++ /dev/null @@ -1,498 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe.each(['sqlite' as const, 'postgresql' as const])('Json field CRUD - %p', (provider) => { - let dbUrl: string; - let prisma: any; - - beforeEach(async () => { - if (provider === 'postgresql') { - dbUrl = await createPostgresDb('json-field-typing'); - } - }); - - afterEach(async () => { - if (provider === 'postgresql') { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb(dbUrl); - } - }); - - it('works with simple cases', async () => { - const params = await loadSchema( - ` - type Address { - city String - } - - type Profile { - age Int - address Address? - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - title String - user User @relation(fields: [userId], references: [id]) - userId Int - } - `, - { - provider, - dbUrl, - enhancements: ['validation'], - } - ); - - prisma = params.prisma; - const db = params.enhance(); - - // expecting object - await expect(db.user.create({ data: { profile: 1 } })).toBeRejectedByPolicy(); - await expect(db.user.create({ data: { profile: [{ age: 18 }] } })).toBeRejectedByPolicy(); - await expect(db.user.create({ data: { profile: { myAge: 18 } } })).toBeRejectedByPolicy(); - await expect(db.user.create({ data: { profile: { address: { city: 'NY' } } } })).toBeRejectedByPolicy(); - await expect(db.user.create({ data: { profile: { age: 18, address: { x: 1 } } } })).toBeRejectedByPolicy(); - - await expect( - db.user.create({ data: { profile: { age: 18 }, posts: { create: { title: 'Post1' } } } }) - ).resolves.toMatchObject({ - profile: { age: 18 }, - }); - await expect( - db.user.create({ - data: { profile: { age: 20, address: { city: 'NY' } }, posts: { create: { title: 'Post1' } } }, - }) - ).resolves.toMatchObject({ - profile: { age: 20, address: { city: 'NY' } }, - }); - }); - - it('works with array', async () => { - const params = await loadSchema( - ` - type Address { - city String - } - - type Profile { - age Int - address Address? - } - - model User { - id Int @id @default(autoincrement()) - profiles Profile[] @json - @@allow('all', true) - } - `, - { - provider, - dbUrl, - } - ); - - prisma = params.prisma; - const db = params.enhance(); - - // expecting array - await expect( - db.user.create({ data: { profiles: { age: 18, address: { city: 'NY' } } } }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ data: { profiles: [{ age: 18, address: { city: 'NY' } }] } }) - ).resolves.toMatchObject({ - profiles: expect.arrayContaining([expect.objectContaining({ age: 18, address: { city: 'NY' } })]), - }); - }); - - it('respects validation rules', async () => { - const params = await loadSchema( - ` - type Address { - city String @length(2, 10) - } - - type Profile { - age Int @gte(18) - address Address? - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - foo Foo? - @@allow('all', true) - } - - model Foo { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - @@allow('all', true) - } - `, - { - provider, - dbUrl, - } - ); - - prisma = params.prisma; - const db = params.enhance(); - - // create - await expect(db.user.create({ data: { profile: { age: 10 } } })).toBeRejectedByPolicy(); - await expect(db.user.create({ data: { profile: { age: 18, address: { city: 'N' } } } })).toBeRejectedByPolicy(); - const u1 = await db.user.create({ data: { profile: { age: 18, address: { city: 'NY' } } } }); - expect(u1).toMatchObject({ - profile: { age: 18, address: { city: 'NY' } }, - }); - - // update - await expect(db.user.update({ where: { id: u1.id }, data: { profile: { age: 10 } } })).toBeRejectedByPolicy(); - await expect( - db.user.update({ where: { id: u1.id }, data: { profile: { age: 20, address: { city: 'B' } } } }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ where: { id: u1.id }, data: { profile: { age: 20, address: { city: 'BJ' } } } }) - ).resolves.toMatchObject({ - profile: { age: 20, address: { city: 'BJ' } }, - }); - - // nested create - await expect(db.foo.create({ data: { user: { create: { profile: { age: 10 } } } } })).toBeRejectedByPolicy(); - await expect(db.foo.create({ data: { user: { create: { profile: { age: 20 } } } } })).toResolveTruthy(); - - // upsert - await expect( - db.user.upsert({ where: { id: 10 }, create: { id: 10, profile: { age: 10 } }, update: {} }) - ).toBeRejectedByPolicy(); - await expect( - db.user.upsert({ where: { id: 10 }, create: { id: 10, profile: { age: 20 } }, update: {} }) - ).toResolveTruthy(); - await expect( - db.user.upsert({ - where: { id: 10 }, - create: { id: 10, profile: { age: 20 } }, - update: { profile: { age: 10 } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.upsert({ - where: { id: 10 }, - create: { id: 10, profile: { age: 20 } }, - update: { profile: { age: 20 } }, - }) - ).toResolveTruthy(); - }); - - it('respects refine validation rules', async () => { - const params = await loadSchema( - ` - type Address { - city String @length(2, 10) - } - - type Profile { - age Int @gte(18) - address Address? - @@validate(age > 18 && length(address.city, 2, 2)) - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - `, - { - provider, - dbUrl, - } - ); - - prisma = params.prisma; - const schema = params.zodSchemas.models.ProfileSchema; - - expect(schema.safeParse({ age: 10, address: { city: 'NY' } })).toMatchObject({ success: false }); - expect(schema.safeParse({ age: 20, address: { city: 'NYC' } })).toMatchObject({ success: false }); - expect(schema.safeParse({ age: 20, address: { city: 'NY' } })).toMatchObject({ success: true }); - - const db = params.enhance(); - await expect(db.user.create({ data: { profile: { age: 10 } } })).toBeRejectedByPolicy(); - await expect( - db.user.create({ data: { profile: { age: 20, address: { city: 'NYC' } } } }) - ).toBeRejectedByPolicy(); - await expect(db.user.create({ data: { profile: { age: 20, address: { city: 'NY' } } } })).toResolveTruthy(); - }); - - it('respects enums used by data models', async () => { - const params = await loadSchema( - ` - enum Role { - USER - ADMIN - } - - type Profile { - role Role - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - - model Foo { - id Int @id @default(autoincrement()) - role Role - } - `, - { - provider, - dbUrl, - } - ); - - prisma = params.prisma; - const db = params.enhance(); - - await expect(db.user.create({ data: { profile: { role: 'MANAGER' } } })).toBeRejectedByPolicy(); - await expect(db.user.create({ data: { profile: { role: 'ADMIN' } } })).resolves.toMatchObject({ - profile: { role: 'ADMIN' }, - }); - await expect(db.user.findFirst()).resolves.toMatchObject({ - profile: { role: 'ADMIN' }, - }); - }); - - it('respects enums unused by data models', async () => { - const params = await loadSchema( - ` - enum Role { - USER - ADMIN - } - - type Profile { - role Role - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - `, - { - provider, - dbUrl, - } - ); - - prisma = params.prisma; - const db = params.enhance(); - - await expect(db.user.create({ data: { profile: { role: 'MANAGER' } } })).toBeRejectedByPolicy(); - await expect(db.user.create({ data: { profile: { role: 'ADMIN' } } })).resolves.toMatchObject({ - profile: { role: 'ADMIN' }, - }); - await expect(db.user.findFirst()).resolves.toMatchObject({ - profile: { role: 'ADMIN' }, - }); - }); - - it('respects @default', async () => { - const params = await loadSchema( - ` - type Address { - state String - city String @default('Issaquah') - } - - type Profile { - createdAt DateTime @default(now()) - address Address? - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - `, - { - provider, - dbUrl, - } - ); - - prisma = params.prisma; - const db = params.enhance(); - - // default value - await expect(db.user.create({ data: { profile: { address: { state: 'WA' } } } })).resolves.toMatchObject({ - profile: { address: { state: 'WA', city: 'Issaquah' }, createdAt: expect.any(Date) }, - }); - - // override default - await expect( - db.user.create({ data: { profile: { address: { state: 'WA', city: 'Seattle' } } } }) - ).resolves.toMatchObject({ - profile: { address: { state: 'WA', city: 'Seattle' } }, - }); - }); - - it('works auth() in @default', async () => { - const params = await loadSchema( - ` - type NestedProfile { - userId Int @default(auth().id) - } - - type Profile { - ownerId Int @default(auth().id) - nested NestedProfile - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - `, - { - provider, - dbUrl, - } - ); - - prisma = params.prisma; - - const db = params.enhance({ id: 1 }); - const u1 = await db.user.create({ data: { profile: { nested: {} } } }); - expect(u1.profile.ownerId).toBe(1); - expect(u1.profile.nested.userId).toBe(1); - - const u2 = await db.user.create({ data: { profile: { ownerId: 2, nested: { userId: 3 } } } }); - expect(u2.profile.ownerId).toBe(2); - expect(u2.profile.nested.userId).toBe(3); - }); - - it('works with recursive types', async () => { - const params = await loadSchema( - ` - type Content { - type String - content Content[]? - text String? - } - - model Post { - id Int @id @default(autoincrement()) - content Content @json - @@allow('all', true) - } - `, - { - provider, - dbUrl, - } - ); - - prisma = params.prisma; - const db = params.enhance(); - const post = await db.post.create({ - data: { - content: { - type: 'text', - content: [ - { - type: 'text', - content: [ - { - type: 'text', - text: 'hello', - }, - ], - }, - ], - }, - }, - }); - - await expect(post.content.content[0].content[0].text).toBe('hello'); - }); - - it('works with Prisma.skip', async () => { - const params = await loadSchema( - ` - type Profile { - foo Int - bar String - } - - model User { - id Int @id @default(autoincrement()) - name String - profile Profile @json - @@allow('all', true) - } - `, - { - provider, - dbUrl, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { enhance } from '.zenstack/enhance'; -import { Prisma, PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - // @ts-expect-error Non optional JSON fields should not be skippable in the create call - db.user.create({ data: { name: 'test', profile: Prisma.skip } }); - - const u = await db.user.create({ data: { name: 'test', profile: { foo: 18, bar: 'test' } } }); - await db.user.update({ where: { id: u.id }, data: { profile: Prisma.skip } }); -} - `, - }, - ], - } - ); - - prisma = params.prisma; - const skip = params.prismaModule.Prisma.skip; - const db = params.enhance(); - - const user = await db.user.create({ data: { name: 'test', profile: { foo: 18, bar: 'test' } } }); - - await expect( - db.user.update({ - where: { id: user.id }, - data: { profile: skip }, - }) - ).resolves.toMatchObject({ - id: user.id, - name: 'test', - profile: { - foo: 18, - bar: 'test', - }, - }); - }); -}); diff --git a/tests/integration/tests/enhancements/json/typing.test.ts b/tests/integration/tests/enhancements/json/typing.test.ts deleted file mode 100644 index 607daa54f..000000000 --- a/tests/integration/tests/enhancements/json/typing.test.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe.each(['sqlite' as const, 'postgresql' as const])('JSON field typing - %p', (provider) => { - it('works with simple field', async () => { - await loadSchema( - ` - type Profile { - age Int @gt(0) - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - posts Post[] - @@allow('all', true) - } - - model Post { - id Int @id @default(autoincrement()) - title String - user User @relation(fields: [userId], references: [id]) - userId Int - } - `, - { - provider, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { enhance } from '.zenstack/enhance'; -import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - const u = await db.user.create({ data: { profile: { age: 18 }, posts: { create: { title: 'Post1' }} } }); - console.log(u.profile.age); - const u1 = await db.user.findUnique({ where: { id: u.id } }); - console.log(u1?.profile.age); - const u2 = await db.user.findMany({include: { posts: true }}); - console.log(u2[0].profile.age); -} - `, - }, - ], - } - ); - }); - - it('works with optional field', async () => { - await loadSchema( - ` - type Profile { - age Int @gt(0) - } - - model User { - id Int @id @default(autoincrement()) - profile Profile? @json - @@allow('all', true) - } - `, - { - provider, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { enhance } from '.zenstack/enhance'; -import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - const u = await db.user.create({ data: { profile: { age: 18 } } }); - console.log(u.profile?.age); - const u1 = await db.user.findUnique({ where: { id: u.id } }); - console.log(u1?.profile?.age); - const u2 = await db.user.findMany(); - console.log(u2[0].profile?.age); -} - `, - }, - ], - } - ); - }); - - it('works with array field', async () => { - await loadSchema( - ` - type Profile { - age Int @gt(0) - } - - model User { - id Int @id @default(autoincrement()) - profiles Profile[] @json - @@allow('all', true) - } - `, - { - provider, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { enhance } from '.zenstack/enhance'; -import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - const u = await db.user.create({ data: { profiles: [{ age: 18 }] } }); - console.log(u.profiles[0].age); - const u1 = await db.user.findUnique({ where: { id: u.id } }); - console.log(u1?.profiles[0].age); - const u2 = await db.user.findMany(); - console.log(u2[0].profiles[0].age); -} - `, - }, - ], - } - ); - }); - - it('works with type nesting', async () => { - await loadSchema( - ` - type Profile { - age Int @gt(0) - address Address? - } - - type Address { - city String - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - `, - { - provider, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { enhance } from '.zenstack/enhance'; -import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - const u = await db.user.create({ data: { profile: { age: 18, address: { city: 'Issaquah' } } } }); - console.log(u.profile.address?.city); - const u1 = await db.user.findUnique({ where: { id: u.id } }); - console.log(u1?.profile.address?.city); - const u2 = await db.user.findMany(); - console.log(u2[0].profile.address?.city); - await db.user.create({ data: { profile: { age: 20 } } }); -} - `, - }, - ], - } - ); - }); - - it('works with enums used in models', async () => { - await loadSchema( - ` - enum Role { - USER - ADMIN - } - - type Profile { - role Role - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - - model Foo { - id Int @id @default(autoincrement()) - role Role - } - `, - { - provider, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import type { Profile } from '.zenstack/models'; -import { enhance } from '.zenstack/enhance'; -import { PrismaClient } from '@prisma/client'; -import { Role } from '@prisma/client'; -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - const profile: Profile = { - role: Role.ADMIN, - } - - await db.user.create({ data: { profile: { role: Role.ADMIN } } }); - const user = await db.user.findFirstOrThrow(); - console.log(user.profile.role === Role.ADMIN); -} -`, - }, - ], - } - ); - }); - - it('works with enums unused in models', async () => { - await loadSchema( - ` - enum Role { - USER - ADMIN - } - - type Profile { - role Role - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - `, - { - provider, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import type { Profile } from '.zenstack/models'; -import { enhance } from '.zenstack/enhance'; -import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - const profile: Profile = { - role: 'ADMIN', - } - - await db.user.create({ data: { profile: { role: 'ADMIN' } } }); - const user = await db.user.findFirstOrThrow(); - console.log(user.profile.role === 'ADMIN'); -} -`, - }, - ], - } - ); - }); - - it('type coverage', async () => { - await loadSchema( - ` - type Profile { - boolean Boolean - bigint BigInt - int Int - float Float - decimal Decimal - string String - bytes Bytes - dateTime DateTime - json Json - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', true) - } - `, - { - provider, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import type { Profile } from '.zenstack/models'; -import { Prisma } from '@prisma/client'; - -async function main() { - const profile: Profile = { - boolean: true, - bigint: BigInt(9007199254740991), - int: 100, - float: 1.23, - decimal: new Prisma.Decimal(1.2345), - string: 'string', - bytes: new Uint8Array([0, 1, 2, 3]), - dateTime: new Date(), - json: { a: 1 }, - } -} - `, - }, - ], - } - ); - }); - - it('supports recursive definition', async () => { - await loadSchema( - ` - type Content { - type String - content Content[]? - text String? - } - - model Post { - id Int @id @default(autoincrement()) - content Content @json - } - `, - { - provider, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import type { Content } from '.zenstack/models'; -import { enhance } from '.zenstack/enhance'; -import { PrismaClient } from '@prisma/client'; - -async function main() { - const content: Content = { - type: 'text', - content: [ - { - type: 'text', - content: [ - { - type: 'text', - text: 'hello', - }, - ], - }, - ], - } - - const db = enhance(new PrismaClient()); - const post = await db.post.create({ data: { content } }); - console.log(post.content.content?.[0].content?.[0].text); -} - `, - }, - ], - } - ); - }); - - it('supports @db.Json and @db.JsonB', async () => { - await loadSchema( - ` - type Profile { - age Int @gt(0) - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json @db.Json - posts Post[] - @@allow('all', true) - } - - type Meta { - description String - } - - model Post { - id Int @id @default(autoincrement()) - title String - user User @relation(fields: [userId], references: [id]) - userId Int - meta Meta @json @db.JsonB - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - }); -}); diff --git a/tests/integration/tests/enhancements/json/validation.test.ts b/tests/integration/tests/enhancements/json/validation.test.ts deleted file mode 100644 index a398234f9..000000000 --- a/tests/integration/tests/enhancements/json/validation.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { loadModel, loadModelWithError, loadSchema } from '@zenstackhq/testtools'; - -describe('JSON field typing', () => { - it('requires field to have @json attribute', async () => { - await expect( - loadModelWithError( - ` - type Profile { - age Int @gt(0) - } - - model User { - id Int @id @default(autoincrement()) - profile Profile - @@allow('all', true) - } - ` - ) - ).resolves.toContain('Custom-typed field must have @json attribute'); - }); - - it('disallows normal member accesses in policy rules', async () => { - await expect( - loadModelWithError( - ` - type Profile { - age Int @gt(0) - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', profile.age > 18) - } - ` - ) - ).resolves.toContain(`Could not resolve reference to MemberAccessTarget named 'age'.`); - }); - - it('allows auth member accesses in policy rules', async () => { - await expect( - loadModel( - ` - type Profile { - age Int @gt(0) - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', auth().profile.age > 18) - } - ` - ) - ).toResolveTruthy(); - }); - - it('disallows normal collection accesses in policy rules', async () => { - await expect( - loadModelWithError( - ` - type Profile { - roles Role[] - } - - type Role { - name String - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', profile.roles?[name == 'ADMIN']) - } - ` - ) - ).resolves.toContain(`Could not resolve reference to MemberAccessTarget named 'roles'.`); - - await expect( - loadModelWithError( - ` - type Profile { - role String - } - - model User { - id Int @id @default(autoincrement()) - profiles Profile[] @json - @@allow('all', profiles?[role == 'ADMIN']) - } - ` - ) - ).resolves.toContain(`Could not resolve reference to ReferenceTarget named 'role'.`); - }); - - it('disallows auth collection accesses in policy rules', async () => { - await expect( - loadModel( - ` - type Profile { - roles Role[] - } - - type Role { - name String - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - @@allow('all', auth().profile.roles?[name == 'ADMIN']) - } - ` - ) - ).toResolveTruthy(); - }); - - it('only allows whitelisted type-level attributes', async () => { - await expect( - loadModel( - ` - type User { - id Int @id - @@auth - } - ` - ) - ).toResolveTruthy(); - - await expect( - loadModelWithError( - ` - type User { - id Int @id - @@allow('all', true) - } - ` - ) - ).resolves.toContain('attribute "@@allow" cannot be used on type declarations'); - }); -}); diff --git a/tests/integration/tests/enhancements/proxy/extension-context.test.ts b/tests/integration/tests/enhancements/proxy/extension-context.test.ts deleted file mode 100644 index f84fd1e84..000000000 --- a/tests/integration/tests/enhancements/proxy/extension-context.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Proxy Extension Context', () => { - it('works', async () => { - const { enhance } = await loadSchema( - ` - model Counter { - model String @unique - value Int - - @@allow('all', true) - } - - model Address { - id String @id @default(cuid()) - city String - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - const dbExtended = db.$extends({ - client: { - $one() { - return 1; - } - }, - model: { - $allModels: { - async createWithCounter(this: any, args: any) { - const modelName = this.$name; - const dbOrTx = this.$parent; - - // prisma exposes some internal properties, makes sure these are still preserved - expect(dbOrTx._engine).toBeDefined(); - - const fn = async (tx: any) => { - const counter = await tx.counter.findUnique({ - where: { model: modelName }, - }); - - await tx.counter.upsert({ - where: { model: modelName }, - update: { value: (counter?.value ?? 0) + tx.$one() }, - create: { model: modelName, value: tx.$one() }, - }); - - return tx[modelName].create(args); - }; - - if (dbOrTx['$transaction']) { - // not running in a transaction, so we need to create a new transaction - return dbOrTx.$transaction(fn); - } - - return fn(dbOrTx); - }, - }, - }, - }); - - const cities = ['Vienna', 'New York', 'Delhi']; - - await Promise.all([ - ...cities.map((city) => dbExtended.address.createWithCounter({ data: { city } })), - ...cities.map((city) => - dbExtended.$transaction((tx: any) => tx.address.createWithCounter({ data: { city: `${city}$tx` } })) - ), - ]); - - await expect(dbExtended.counter.findUniqueOrThrow({ where: { model: 'Address' } })).resolves.toMatchObject({ - model: 'Address', - value: cities.length * 2, - }); - }); -}); diff --git a/tests/integration/tests/enhancements/typing/enhancement-typing.test.ts b/tests/integration/tests/enhancements/typing/enhancement-typing.test.ts deleted file mode 100644 index d29897a71..000000000 --- a/tests/integration/tests/enhancements/typing/enhancement-typing.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Enhancement typing tests', () => { - it('infers correct typing', async () => { - await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) - } - `, - { - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient } from '@prisma/client'; -import type { Enhanced } from '.zenstack/enhance'; - -async function withoutClientExtension() { - const prisma = new PrismaClient(); - const db = {} as any as Enhanced; - // note that "author" becomes optional - const r = await db.post.create({ data: { title: 'Post1' }}); - console.log(r); -} - -async function withClientExtension() { - const prisma = (new PrismaClient()) - .$extends({ - client: { - $log: (message: string) => { - console.log(message); - }, - }, - }); - const db = {} as any as Enhanced; - // note that "author" becomes optional - const r = await db.post.create({ data: { title: 'Post1' }}); - console.log(r); - - // note that "$log" is preserved - db.$log('hello'); -} - `, - }, - ], - } - ); - }); -}); diff --git a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts b/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts deleted file mode 100644 index 544198c32..000000000 --- a/tests/integration/tests/enhancements/with-delegate/enhanced-client.test.ts +++ /dev/null @@ -1,1655 +0,0 @@ -import { PrismaErrorCode } from '@zenstackhq/runtime'; -import { loadSchema } from '@zenstackhq/testtools'; -import { POLYMORPHIC_MANY_TO_MANY_SCHEMA, POLYMORPHIC_SCHEMA } from './utils'; - -describe('Polymorphism Test', () => { - const schema = POLYMORPHIC_SCHEMA; - - async function setup() { - const { enhance } = await loadSchema(schema, { enhancements: ['delegate'] }); - const db = enhance(); - - const user = await db.user.create({ data: { id: 1 } }); - - const video = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - - const videoWithOwner = await db.ratedVideo.findUnique({ where: { id: video.id }, include: { owner: true } }); - - return { db, video, user, videoWithOwner }; - } - - it('create hierarchy', async () => { - const { enhance } = await loadSchema(schema, { enhancements: ['delegate'] }); - const db = enhance(); - - const user = await db.user.create({ data: { id: 1 } }); - - const video = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - include: { owner: true }, - }); - - expect(video).toMatchObject({ - viewCount: 1, - duration: 100, - url: 'xyz', - rating: 100, - assetType: 'Video', - videoType: 'RatedVideo', - owner: user, - }); - - await expect(db.asset.create({ data: { type: 'Video' } })).rejects.toThrow('is a delegate'); - await expect(db.video.create({ data: { type: 'RatedVideo' } })).rejects.toThrow('is a delegate'); - - const image = await db.image.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, format: 'png' }, - include: { owner: true }, - }); - expect(image).toMatchObject({ - viewCount: 1, - format: 'png', - assetType: 'Image', - owner: user, - }); - - // create in a nested payload - const gallery = await db.gallery.create({ - data: { - images: { - create: [ - { owner: { connect: { id: user.id } }, format: 'png', viewCount: 1 }, - { owner: { connect: { id: user.id } }, format: 'jpg', viewCount: 2 }, - ], - }, - }, - include: { images: { include: { owner: true } } }, - }); - expect(gallery.images).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - format: 'png', - assetType: 'Image', - viewCount: 1, - owner: user, - }), - expect.objectContaining({ - format: 'jpg', - assetType: 'Image', - viewCount: 2, - owner: user, - }), - ]) - ); - }); - - it('create with base all defaults', async () => { - const { enhance } = await loadSchema( - ` - model Base { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - type String - - @@delegate(type) - } - - model Foo extends Base { - name String - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const r = await db.foo.create({ data: { name: 'foo' } }); - expect(r).toMatchObject({ name: 'foo', type: 'Foo', id: expect.any(Number), createdAt: expect.any(Date) }); - }); - - it('create with nesting', async () => { - const { enhance } = await loadSchema(schema, { enhancements: ['delegate'] }); - const db = enhance(); - - // nested create a relation from base - await expect( - db.ratedVideo.create({ - data: { owner: { create: { id: 2 } }, url: 'xyz', rating: 200, duration: 200 }, - include: { owner: true }, - }) - ).resolves.toMatchObject({ owner: { id: 2 } }); - }); - - it('create many polymorphic model', async () => { - const { enhance } = await loadSchema(schema, { enhancements: ['delegate'] }); - const db = enhance(); - - await expect( - db.ratedVideo.createMany({ data: { viewCount: 1, duration: 100, url: 'xyz', rating: 100 } }) - ).resolves.toMatchObject({ count: 1 }); - - await expect( - db.ratedVideo.createManyAndReturn({ data: { viewCount: 1, duration: 100, url: 'xyz', rating: 100 } }) - ).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - assetType: 'Video', - videoType: 'RatedVideo', - viewCount: 1, - duration: 100, - url: 'xyz', - rating: 100, - }), - ]) - ); - - await expect( - db.ratedVideo.createMany({ - data: [ - { viewCount: 2, duration: 200, url: 'xyz', rating: 100 }, - { viewCount: 3, duration: 300, url: 'xyz', rating: 200 }, - ], - }) - ).resolves.toMatchObject({ count: 2 }); - - await expect( - db.ratedVideo.createManyAndReturn({ - data: [ - { viewCount: 2, duration: 200, url: 'xyz', rating: 100 }, - { viewCount: 3, duration: 300, url: 'xyz', rating: 200 }, - ], - select: { videoType: true, viewCount: true, rating: true }, - }) - ).resolves.toEqual( - expect.arrayContaining([ - { videoType: 'RatedVideo', viewCount: 2, rating: 100 }, - { videoType: 'RatedVideo', viewCount: 3, rating: 200 }, - ]) - ); - }); - - it('create many polymorphic relation', async () => { - const { enhance } = await loadSchema(schema, { enhancements: ['delegate'] }); - const db = enhance(); - - const video1 = await db.ratedVideo.create({ - data: { viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - await expect( - db.user.createMany({ data: { id: 1, assets: { connect: { id: video1.id } } } }) - ).resolves.toMatchObject({ count: 1 }); - - const video2 = await db.ratedVideo.create({ - data: { viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - await expect( - db.user.createMany({ data: [{ id: 2, assets: { connect: { id: video2.id } } }, { id: 3 }] }) - ).resolves.toMatchObject({ count: 2 }); - }); - - it('create concrete with explicit id', async () => { - const { enhance } = await loadSchema(schema, { enhancements: ['delegate'] }); - const db = enhance(); - - await expect( - db.ratedVideo.create({ data: { id: 1, duration: 100, url: 'xyz', rating: 5 } }) - ).resolves.toMatchObject({ - id: 1, - duration: 100, - url: 'xyz', - rating: 5, - assetType: 'Video', - videoType: 'RatedVideo', - }); - }); - - it('read with concrete', async () => { - const { db, user, video } = await setup(); - - // find with include - let found = await db.ratedVideo.findFirst({ include: { owner: true } }); - expect(found).toMatchObject(video); - expect(found.owner).toMatchObject(user); - - // find with select - found = await db.ratedVideo.findFirst({ select: { id: true, createdAt: true, url: true, rating: true } }); - expect(found).toMatchObject({ id: video.id, createdAt: video.createdAt, url: video.url, rating: video.rating }); - - // findFirstOrThrow - found = await db.ratedVideo.findFirstOrThrow(); - expect(found).toMatchObject(video); - await expect( - db.ratedVideo.findFirstOrThrow({ - where: { id: video.id + 1 }, - }) - ).rejects.toThrow(); - - // findUnique - found = await db.ratedVideo.findUnique({ - where: { id: video.id }, - }); - expect(found).toMatchObject(video); - - // findUniqueOrThrow - found = await db.ratedVideo.findUniqueOrThrow({ - where: { id: video.id }, - }); - expect(found).toMatchObject(video); - await expect( - db.ratedVideo.findUniqueOrThrow({ - where: { id: video.id + 1 }, - }) - ).rejects.toThrow(); - - // findMany - let items = await db.ratedVideo.findMany(); - expect(items).toHaveLength(1); - expect(items[0]).toMatchObject(video); - - // findMany not found - items = await db.ratedVideo.findMany({ where: { id: video.id + 1 } }); - expect(items).toHaveLength(0); - - // findMany with select - items = await db.ratedVideo.findMany({ select: { id: true, createdAt: true, url: true, rating: true } }); - expect(items).toHaveLength(1); - expect(items[0]).toMatchObject({ - id: video.id, - createdAt: video.createdAt, - url: video.url, - rating: video.rating, - }); - - // find with base filter - found = await db.ratedVideo.findFirst({ where: { viewCount: video.viewCount } }); - expect(found).toMatchObject(video); - found = await db.ratedVideo.findFirst({ where: { url: video.url, owner: { id: user.id } } }); - expect(found).toMatchObject(video); - - // image: single inheritance - const image = await db.image.create({ - data: { owner: { connect: { id: 1 } }, viewCount: 1, format: 'png' }, - include: { owner: true }, - }); - const readImage = await db.image.findFirst({ include: { owner: true } }); - expect(readImage).toMatchObject(image); - expect(readImage.owner).toMatchObject(user); - }); - - it('read with base', async () => { - const { db, user, video: r } = await setup(); - - let video = await db.video.findFirst({ where: { duration: r.duration }, include: { owner: true } }); - expect(video).toMatchObject({ - ...r, - assetType: 'Video', - videoType: 'RatedVideo', - }); - expect(video.owner).toMatchObject(user); - - const asset = await db.asset.findFirst({ where: { viewCount: r.viewCount }, include: { owner: true } }); - expect(asset).toMatchObject({ - ...r, - assetType: 'Video', - videoType: 'RatedVideo', - owner: expect.objectContaining(user), - }); - - const userWithAssets = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); - expect(userWithAssets.assets[0]).toMatchObject(r); - - const image = await db.image.create({ - data: { owner: { connect: { id: 1 } }, viewCount: 1, format: 'png' }, - include: { owner: true }, - }); - const imgAsset = await db.asset.findFirst({ where: { assetType: 'Image' }, include: { owner: true } }); - expect(imgAsset).toMatchObject({ - id: image.id, - createdAt: image.createdAt, - assetType: 'Image', - viewCount: image.viewCount, - format: 'png', - owner: expect.objectContaining(user), - }); - }); - - it('read with compound filter', async () => { - const { enhance } = await loadSchema( - ` - model Base { - id Int @id @default(autoincrement()) - type String - viewCount Int - @@delegate(type) - } - - model Foo extends Base { - name String - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - await db.foo.create({ data: { name: 'foo1', viewCount: 0 } }); - await db.foo.create({ data: { name: 'foo2', viewCount: 1 } }); - - await expect(db.foo.findMany({ where: { viewCount: { gt: 0 } } })).resolves.toHaveLength(1); - await expect(db.foo.findMany({ where: { AND: { viewCount: { gt: 0 } } } })).resolves.toHaveLength(1); - await expect(db.foo.findMany({ where: { AND: [{ viewCount: { gt: 0 } }] } })).resolves.toHaveLength(1); - await expect(db.foo.findMany({ where: { OR: [{ viewCount: { gt: 0 } }] } })).resolves.toHaveLength(1); - await expect(db.foo.findMany({ where: { NOT: { viewCount: { lte: 0 } } } })).resolves.toHaveLength(1); - }); - - it('read with nested filter', async () => { - const { enhance } = await loadSchema( - ` - model Base { - id Int @id @default(autoincrement()) - type String - viewCount Int - @@delegate(type) - } - - model Foo extends Base { - name String - bar Bar? - } - - model Bar extends Base { - foo Foo @relation(fields: [fooId], references: [id]) - fooId Int @unique - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - - await db.bar.create({ - data: { foo: { create: { name: 'foo', viewCount: 2 } }, viewCount: 1 }, - }); - - await expect( - db.bar.findMany({ - where: { viewCount: { gt: 0 }, foo: { viewCount: { gt: 1 } } }, - }) - ).resolves.toHaveLength(1); - }); - - it('read with counting relation defined in base', async () => { - const { enhance } = await loadSchema( - ` - - model A { - id Int @id @default(autoincrement()) - type String - bs B[] - cs C[] - @@delegate(type) - } - - model A1 extends A { - a1 Int - type1 String - @@delegate(type1) - } - - model A2 extends A1 { - a2 Int - } - - model B { - id Int @id @default(autoincrement()) - a A @relation(fields: [aId], references: [id]) - aId Int - b Int - } - - model C { - id Int @id @default(autoincrement()) - a A @relation(fields: [aId], references: [id]) - aId Int - c Int - } - `, - { enhancements: ['delegate'] } - ); - const db = enhance(); - - const a2 = await db.a2.create({ - data: { a1: 1, a2: 2, bs: { create: [{ b: 1 }, { b: 2 }] }, cs: { create: [{ c: 1 }] } }, - include: { _count: { select: { bs: true } } }, - }); - expect(a2).toMatchObject({ a1: 1, a2: 2, _count: { bs: 2 } }); - - await expect( - db.a2.findFirst({ select: { a1: true, _count: { select: { bs: true } } } }) - ).resolves.toStrictEqual({ - a1: 1, - _count: { bs: 2 }, - }); - - await expect(db.a.findFirst({ select: { _count: { select: { bs: true, cs: true } } } })).resolves.toMatchObject( - { - _count: { bs: 2, cs: 1 }, - } - ); - }); - - it('order by base fields', async () => { - const { db, user } = await setup(); - - await expect( - db.video.findMany({ - orderBy: { viewCount: 'desc' }, - }) - ).resolves.toHaveLength(1); - - await expect( - db.ratedVideo.findMany({ - orderBy: { duration: 'asc' }, - }) - ).resolves.toHaveLength(1); - - await expect( - db.user.findMany({ - orderBy: { assets: { _count: 'desc' } }, - }) - ).resolves.toHaveLength(1); - - await expect( - db.user.findUnique({ - where: { id: user.id }, - include: { - ratedVideos: { - orderBy: { - viewCount: 'desc', - }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('update simple', async () => { - const { db, videoWithOwner: video } = await setup(); - - const read = await db.ratedVideo.findUnique({ where: { id: video.id } }); - - // update with concrete - let updated = await db.ratedVideo.update({ - where: { id: video.id }, - data: { rating: 200 }, - include: { owner: true }, - }); - expect(updated.rating).toBe(200); - expect(updated.owner).toBeTruthy(); - expect(updated.updatedAt.getTime()).toBeGreaterThan(read.updatedAt.getTime()); - - // update with base - updated = await db.video.update({ - where: { id: video.id }, - data: { duration: 200 }, - select: { duration: true, createdAt: true }, - }); - expect(updated.duration).toBe(200); - expect(updated.createdAt).toBeTruthy(); - - // update with base - updated = await db.asset.update({ - where: { id: video.id }, - data: { viewCount: 200 }, - }); - expect(updated.viewCount).toBe(200); - - // set discriminator - await expect(db.ratedVideo.update({ where: { id: video.id }, data: { assetType: 'Image' } })).rejects.toThrow( - 'is a discriminator' - ); - await expect( - db.ratedVideo.update({ where: { id: video.id }, data: { videoType: 'RatedVideo' } }) - ).rejects.toThrow('is a discriminator'); - }); - - it('update nested create', async () => { - const { db, videoWithOwner: video, user } = await setup(); - - // create delegate not allowed - await expect( - db.user.update({ - where: { id: user.id }, - data: { - assets: { - create: { viewCount: 1 }, - }, - }, - include: { assets: true }, - }) - ).rejects.toThrow('is a delegate'); - - // create concrete - await expect( - db.user.update({ - where: { id: user.id }, - data: { - ratedVideos: { - create: { - viewCount: 1, - duration: 100, - url: 'xyz', - rating: 100, - owner: { connect: { id: user.id } }, - }, - }, - }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([ - expect.objectContaining({ viewCount: 1, duration: 100, url: 'xyz', rating: 100 }), - ]), - }); - - // nested create a relation from base - const newVideo = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - await expect( - db.ratedVideo.update({ - where: { id: newVideo.id }, - data: { owner: { create: { id: 2 } }, url: 'xyz', duration: 200, rating: 200 }, - include: { owner: true }, - }) - ).resolves.toMatchObject({ owner: { id: 2 } }); - }); - - it('update nested updateOne', async () => { - const { db, videoWithOwner: video, user } = await setup(); - - // update - let updated = await db.asset.update({ - where: { id: video.id }, - data: { owner: { update: { level: 1 } } }, - include: { owner: true }, - }); - expect(updated.owner.level).toBe(1); - - updated = await db.video.update({ - where: { id: video.id }, - data: { duration: 300, owner: { update: { level: 2 } } }, - include: { owner: true }, - }); - expect(updated.duration).toBe(300); - expect(updated.owner.level).toBe(2); - - updated = await db.ratedVideo.update({ - where: { id: video.id }, - data: { rating: 300, owner: { update: { level: 3 } } }, - include: { owner: true }, - }); - expect(updated.rating).toBe(300); - expect(updated.owner.level).toBe(3); - }); - - it('update nested updateMany', async () => { - const { db } = await setup(); - - const user = await db.user.create({ - data: { - email: 'a@b.com', - ratedVideos: { - create: { id: 10, url: 'xyz', duration: 1, rating: 111 }, - }, - }, - }); - - // create another user and video - await db.user.create({ - data: { - email: 'b@c.com', - ratedVideos: { - create: { id: 20, url: 'abc', duration: 2, rating: 222 }, - }, - }, - }); - - // updateMany with filter - const read = await db.ratedVideo.findFirst({ where: { duration: 1 } }); - const r = await db.user.update({ - where: { id: user.id }, - data: { - ratedVideos: { updateMany: { where: { duration: 1 }, data: { rating: 333 } } }, - }, - include: { ratedVideos: true }, - }); - expect(r).toMatchObject({ - ratedVideos: expect.arrayContaining([expect.objectContaining({ rating: 333 })]), - }); - expect(r.ratedVideos[0].updatedAt.getTime()).toBeGreaterThan(read.updatedAt.getTime()); - - // updateMany without filter - await expect( - db.user.update({ - where: { email: 'a@b.com' }, - data: { - ratedVideos: { updateMany: { data: { duration: 3 } } }, - }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([expect.objectContaining({ duration: 3 })]), - }); - - // user2's video should not be updated - await expect(db.ratedVideo.findUnique({ where: { id: 20 } })).resolves.toMatchObject({ - duration: 2, - rating: 222, - }); - }); - - it('update nested deleteOne', async () => { - const { db, videoWithOwner: video, user } = await setup(); - - // delete with base - await db.user.update({ - where: { id: user.id }, - data: { assets: { delete: { id: video.id } } }, - }); - await expect(db.asset.findUnique({ where: { id: video.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: video.id } })).resolves.toBeNull(); - await expect(db.ratedVideo.findUnique({ where: { id: video.id } })).resolves.toBeNull(); - - // delete with concrete - let vid = await db.ratedVideo.create({ - data: { - user: { connect: { id: user.id } }, - owner: { connect: { id: user.id } }, - url: 'xyz', - duration: 111, - rating: 222, - }, - }); - await db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { delete: { id: vid.id } } }, - }); - await expect(db.asset.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); - await expect(db.ratedVideo.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); - - // delete with mixed filter - vid = await db.ratedVideo.create({ - data: { - user: { connect: { id: user.id } }, - owner: { connect: { id: user.id } }, - url: 'xyz', - duration: 111, - rating: 222, - }, - }); - await db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { delete: { id: vid.id, duration: 111 } } }, - }); - await expect(db.asset.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); - await expect(db.ratedVideo.findUnique({ where: { id: vid.id } })).resolves.toBeNull(); - - // delete not found - await expect( - db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { delete: { id: vid.id } } }, - }) - ).toBeNotFound(); - }); - - it('update nested deleteMany', async () => { - const { db, videoWithOwner: video, user } = await setup(); - - // delete with base no filter - await db.user.update({ - where: { id: user.id }, - data: { assets: { deleteMany: {} } }, - }); - await expect(db.asset.findUnique({ where: { id: video.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: video.id } })).resolves.toBeNull(); - await expect(db.ratedVideo.findUnique({ where: { id: video.id } })).resolves.toBeNull(); - - // delete with concrete - let vid1 = await db.ratedVideo.create({ - data: { - user: { connect: { id: user.id } }, - owner: { connect: { id: user.id } }, - url: 'abc', - duration: 111, - rating: 111, - }, - }); - let vid2 = await db.ratedVideo.create({ - data: { - user: { connect: { id: user.id } }, - owner: { connect: { id: user.id } }, - url: 'xyz', - duration: 222, - rating: 222, - }, - }); - await db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { deleteMany: { rating: 111 } } }, - }); - await expect(db.asset.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); - await expect(db.ratedVideo.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); - await expect(db.asset.findUnique({ where: { id: vid2.id } })).toResolveTruthy(); - await db.asset.deleteMany(); - - // delete with mixed args - vid1 = await db.ratedVideo.create({ - data: { - user: { connect: { id: user.id } }, - owner: { connect: { id: user.id } }, - url: 'abc', - duration: 111, - rating: 111, - viewCount: 111, - }, - }); - vid2 = await db.ratedVideo.create({ - data: { - user: { connect: { id: user.id } }, - owner: { connect: { id: user.id } }, - url: 'xyz', - duration: 222, - rating: 222, - viewCount: 222, - }, - }); - await db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { deleteMany: { url: 'abc', rating: 111, viewCount: 111 } } }, - }); - await expect(db.asset.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); - await expect(db.ratedVideo.findUnique({ where: { id: vid1.id } })).resolves.toBeNull(); - await expect(db.asset.findUnique({ where: { id: vid2.id } })).toResolveTruthy(); - await db.asset.deleteMany(); - - // delete not found - vid1 = await db.ratedVideo.create({ - data: { - user: { connect: { id: user.id } }, - owner: { connect: { id: user.id } }, - url: 'abc', - duration: 111, - rating: 111, - }, - }); - vid2 = await db.ratedVideo.create({ - data: { - user: { connect: { id: user.id } }, - owner: { connect: { id: user.id } }, - url: 'xyz', - duration: 222, - rating: 222, - }, - }); - await db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { deleteMany: { url: 'abc', rating: 222 } } }, - }); - await expect(db.asset.count()).resolves.toBe(2); - }); - - it('update nested relation manipulation', async () => { - const { db, videoWithOwner: video, user } = await setup(); - - // connect, disconnect with base - await expect( - db.user.update({ - where: { id: user.id }, - data: { assets: { disconnect: { id: video.id } } }, - include: { assets: true }, - }) - ).resolves.toMatchObject({ - assets: expect.arrayContaining([]), - }); - await expect( - db.user.update({ - where: { id: user.id }, - data: { assets: { connect: { id: video.id } } }, - include: { assets: true }, - }) - ).resolves.toMatchObject({ - assets: expect.arrayContaining([expect.objectContaining({ id: video.id })]), - }); - - /// connect, disconnect with concrete - - let vid1 = await db.ratedVideo.create({ - data: { - url: 'abc', - duration: 111, - rating: 111, - }, - }); - let vid2 = await db.ratedVideo.create({ - data: { - url: 'xyz', - duration: 222, - rating: 222, - }, - }); - - // connect not found - await expect( - db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { connect: [{ id: vid2.id + 1 }] } }, - include: { ratedVideos: true }, - }) - ).toBeRejectedWithCode(PrismaErrorCode.REQUIRED_CONNECTED_RECORD_NOT_FOUND); - - // connect found - await expect( - db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { connect: [{ id: vid1.id, duration: vid1.duration, rating: vid1.rating }] } }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([expect.objectContaining({ id: vid1.id })]), - }); - - // connectOrCreate - await expect( - db.user.update({ - where: { id: user.id }, - data: { - ratedVideos: { - connectOrCreate: [ - { - where: { id: vid2.id, duration: 333 }, - create: { - url: 'xyz', - duration: 333, - rating: 333, - }, - }, - ], - }, - }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([expect.objectContaining({ duration: 333 })]), - }); - - // disconnect not found - await expect( - db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { disconnect: [{ id: vid2.id }] } }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([expect.objectContaining({ id: vid1.id })]), - }); - - // disconnect found - await expect( - db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { disconnect: [{ id: vid1.id, duration: vid1.duration, rating: vid1.rating }] } }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([]), - }); - - // set - await expect( - db.user.update({ - where: { id: user.id }, - data: { - ratedVideos: { - set: [ - { id: vid1.id, viewCount: vid1.viewCount }, - { id: vid2.id, viewCount: vid2.viewCount }, - ], - }, - }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([ - expect.objectContaining({ id: vid1.id }), - expect.objectContaining({ id: vid2.id }), - ]), - }); - await expect( - db.user.update({ - where: { id: user.id }, - data: { ratedVideos: { set: [] } }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([]), - }); - await expect( - db.user.update({ - where: { id: user.id }, - data: { - ratedVideos: { - set: { id: vid1.id, viewCount: vid1.viewCount }, - }, - }, - include: { ratedVideos: true }, - }) - ).resolves.toMatchObject({ - ratedVideos: expect.arrayContaining([expect.objectContaining({ id: vid1.id })]), - }); - }); - - it('updateMany', async () => { - const { db, videoWithOwner: video, user } = await setup(); - const otherVideo = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 10000, duration: 10000, url: 'xyz', rating: 10000 }, - }); - - // update only the current level - await expect( - db.ratedVideo.updateMany({ - where: { rating: video.rating, viewCount: video.viewCount }, - data: { rating: 100 }, - }) - ).resolves.toMatchObject({ count: 1 }); - let read = await db.ratedVideo.findUnique({ where: { id: video.id } }); - expect(read).toMatchObject({ rating: 100 }); - - // update with concrete - await expect( - db.ratedVideo.updateMany({ - where: { id: video.id }, - data: { viewCount: 1, duration: 11, rating: 101 }, - }) - ).resolves.toMatchObject({ count: 1 }); - read = await db.ratedVideo.findUnique({ where: { id: video.id } }); - expect(read).toMatchObject({ viewCount: 1, duration: 11, rating: 101 }); - - // update with base - await db.video.updateMany({ - where: { viewCount: 1, duration: 11 }, - data: { viewCount: 2, duration: 12 }, - }); - read = await db.ratedVideo.findUnique({ where: { id: video.id } }); - expect(read).toMatchObject({ viewCount: 2, duration: 12 }); - - // update with base - await db.asset.updateMany({ - where: { viewCount: 2 }, - data: { viewCount: 3 }, - }); - read = await db.ratedVideo.findUnique({ where: { id: video.id } }); - expect(read.viewCount).toBe(3); - - // the other video is unchanged - await expect(await db.ratedVideo.findUnique({ where: { id: otherVideo.id } })).toMatchObject(otherVideo); - - // update with concrete no where - await expect( - db.ratedVideo.updateMany({ - data: { viewCount: 111, duration: 111, rating: 111 }, - }) - ).resolves.toMatchObject({ count: 2 }); - await expect(db.ratedVideo.findUnique({ where: { id: video.id } })).resolves.toMatchObject({ duration: 111 }); - await expect(db.ratedVideo.findUnique({ where: { id: otherVideo.id } })).resolves.toMatchObject({ - duration: 111, - }); - - // set discriminator - await expect(db.ratedVideo.updateMany({ data: { assetType: 'Image' } })).rejects.toThrow('is a discriminator'); - await expect(db.ratedVideo.updateMany({ data: { videoType: 'RatedVideo' } })).rejects.toThrow( - 'is a discriminator' - ); - }); - - it('upsert', async () => { - const { db, videoWithOwner: video, user } = await setup(); - - await expect( - db.asset.upsert({ - where: { id: video.id }, - create: { id: video.id, viewCount: 1 }, - update: { viewCount: 2 }, - }) - ).rejects.toThrow('is a delegate'); - - // update - const read = await db.ratedVideo.findUnique({ where: { id: video.id } }); - const r = await db.ratedVideo.upsert({ - where: { id: video.id }, - create: { - viewCount: 1, - duration: 300, - url: 'xyz', - rating: 100, - owner: { connect: { id: user.id } }, - }, - update: { duration: 200 }, - }); - expect(r).toMatchObject({ - id: video.id, - duration: 200, - }); - expect(r.updatedAt.getTime()).toBeGreaterThan(read.updatedAt.getTime()); - - // create - const created = await db.ratedVideo.upsert({ - where: { id: video.id + 1 }, - create: { viewCount: 1, duration: 300, url: 'xyz', rating: 100, owner: { connect: { id: user.id } } }, - update: { duration: 200 }, - }); - expect(created.id).not.toEqual(video.id); - expect(created.duration).toBe(300); - }); - - it('delete simple', async () => { - let { db, user, video: ratedVideo } = await setup(); - - let deleted = await db.ratedVideo.delete({ - where: { id: ratedVideo.id }, - select: { rating: true, owner: true }, - }); - expect(deleted).toMatchObject({ rating: 100 }); - expect(deleted.owner).toMatchObject(user); - await expect(db.ratedVideo.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - await expect(db.asset.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - - // delete with base - ratedVideo = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - const video = await db.video.findUnique({ where: { id: ratedVideo.id } }); - deleted = await db.video.delete({ where: { id: ratedVideo.id }, include: { owner: true } }); - expect(deleted).toMatchObject(video); - expect(deleted.owner).toMatchObject(user); - await expect(db.ratedVideo.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - await expect(db.asset.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - - // delete with concrete - ratedVideo = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - let asset = await db.asset.findUnique({ where: { id: ratedVideo.id } }); - deleted = await db.video.delete({ where: { id: ratedVideo.id }, include: { owner: true } }); - expect(deleted).toMatchObject(asset); - expect(deleted.owner).toMatchObject(user); - await expect(db.ratedVideo.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - await expect(db.asset.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - - // delete with combined condition - ratedVideo = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - asset = await db.asset.findUnique({ where: { id: ratedVideo.id } }); - deleted = await db.video.delete({ where: { id: ratedVideo.id, viewCount: 1 } }); - expect(deleted).toMatchObject(asset); - await expect(db.ratedVideo.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - await expect(db.asset.findUnique({ where: { id: ratedVideo.id } })).resolves.toBeNull(); - }); - - it('delete cascade', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Base { - id Int @id @default(autoincrement()) - type String - @@delegate(type) - } - - model List extends Base { - name String - items Item[] - } - - model Item extends Base { - name String - list List @relation(fields: [listId], references: [id], onDelete: Cascade) - listId Int - content ItemContent? - } - - model ItemContent extends Base { - name String - item Item @relation(fields: [itemId], references: [id], onDelete: Cascade) - itemId Int @unique - } -`, - { enhancements: ['delegate'], logPrismaQuery: true } - ); - - const db = enhance(); - await db.list.create({ - data: { - id: 1, - name: 'list', - items: { - create: [{ id: 2, name: 'item1', content: { create: { id: 3, name: 'content1' } } }], - }, - }, - }); - - const r = await db.list.delete({ where: { id: 1 }, include: { items: { include: { content: true } } } }); - expect(r).toMatchObject({ items: [{ id: 2 }] }); - await expect(db.item.findUnique({ where: { id: 2 } })).toResolveNull(); - await expect(prisma.base.findUnique({ where: { id: 2 } })).toResolveNull(); - await expect(db.itemContent.findUnique({ where: { id: 3 } })).toResolveNull(); - await expect(prisma.base.findUnique({ where: { id: 3 } })).toResolveNull(); - }); - - it('deleteMany', async () => { - const { enhance } = await loadSchema(schema, { enhancements: ['delegate'] }); - const db = enhance(); - - const user = await db.user.create({ data: { id: 1 } }); - - // no where - let video1 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - let video2 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'xyz', rating: 100 }, - }); - await expect(db.ratedVideo.deleteMany()).resolves.toMatchObject({ count: 2 }); - await expect(db.ratedVideo.findUnique({ where: { id: video1.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: video1.id } })).resolves.toBeNull(); - await expect(db.asset.findUnique({ where: { id: video1.id } })).resolves.toBeNull(); - await expect(db.ratedVideo.findUnique({ where: { id: video2.id } })).resolves.toBeNull(); - await expect(db.video.findUnique({ where: { id: video2.id } })).resolves.toBeNull(); - await expect(db.asset.findUnique({ where: { id: video2.id } })).resolves.toBeNull(); - await expect(db.ratedVideo.count()).resolves.toBe(0); - - // with base - video1 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'abc', rating: 100 }, - }); - video2 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 2, duration: 200, url: 'xyz', rating: 200 }, - }); - await expect(db.asset.deleteMany({ where: { viewCount: 1 } })).resolves.toMatchObject({ count: 1 }); - await expect(db.asset.count()).resolves.toBe(1); - await db.asset.deleteMany(); - - // where current level - video1 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'abc', rating: 100 }, - }); - video2 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 2, duration: 200, url: 'xyz', rating: 200 }, - }); - await expect(db.ratedVideo.deleteMany({ where: { rating: 100 } })).resolves.toMatchObject({ count: 1 }); - await expect(db.ratedVideo.count()).resolves.toBe(1); - await db.ratedVideo.deleteMany(); - - // where mixed with base level - video1 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'abc', rating: 100 }, - }); - video2 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 2, duration: 200, url: 'xyz', rating: 200 }, - }); - await expect(db.ratedVideo.deleteMany({ where: { viewCount: 1, duration: 100 } })).resolves.toMatchObject({ - count: 1, - }); - await expect(db.ratedVideo.count()).resolves.toBe(1); - await db.ratedVideo.deleteMany(); - - // delete not found - video1 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 1, duration: 100, url: 'abc', rating: 100 }, - }); - video2 = await db.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: 2, duration: 200, url: 'xyz', rating: 200 }, - }); - await expect(db.ratedVideo.deleteMany({ where: { viewCount: 2, duration: 100 } })).resolves.toMatchObject({ - count: 0, - }); - await expect(db.ratedVideo.count()).resolves.toBe(2); - }); - - it('aggregate', async () => { - const { db } = await setup(); - - const aggregate = await db.ratedVideo.aggregate({ - _count: true, - _sum: { rating: true }, - where: { viewCount: { gt: 0 }, rating: { gt: 10 } }, - orderBy: { - duration: 'desc', - }, - }); - expect(aggregate).toMatchObject({ _count: 1, _sum: { rating: 100 } }); - - expect(() => db.ratedVideo.aggregate({ _count: true, _sum: { rating: true, viewCount: true } })).toThrow( - 'aggregate with fields from base type is not supported yet' - ); - }); - - it('count', async () => { - const { db } = await setup(); - - let count = await db.ratedVideo.count(); - expect(count).toBe(1); - - count = await db.ratedVideo.count({ - select: { _all: true, rating: true }, - where: { viewCount: { gt: 0 }, rating: { gt: 10 } }, - }); - expect(count).toMatchObject({ _all: 1, rating: 1 }); - - count = await db.ratedVideo.count({ - select: { _all: true, rating: true }, - where: { AND: { viewCount: { gt: 0 }, rating: { gt: 10 } } }, - }); - expect(count).toMatchObject({ _all: 1, rating: 1 }); - - count = await db.ratedVideo.count({ - select: { _all: true, rating: true }, - where: { AND: [{ viewCount: { gt: 0 }, rating: { gt: 10 } }] }, - }); - expect(count).toMatchObject({ _all: 1, rating: 1 }); - - expect(() => db.ratedVideo.count({ select: { rating: true, viewCount: true } })).toThrow( - 'count with fields from base type is not supported yet' - ); - }); - - it('groupBy', async () => { - const { db, video } = await setup(); - - let group = await db.ratedVideo.groupBy({ by: ['rating'] }); - expect(group).toHaveLength(1); - expect(group[0]).toMatchObject({ rating: video.rating }); - - group = await db.ratedVideo.groupBy({ - by: ['id', 'rating'], - where: { viewCount: { gt: 0 }, rating: { gt: 10 } }, - }); - expect(group).toHaveLength(1); - expect(group[0]).toMatchObject({ id: video.id, rating: video.rating }); - - group = await db.ratedVideo.groupBy({ - by: ['id'], - _sum: { rating: true }, - }); - expect(group).toHaveLength(1); - expect(group[0]).toMatchObject({ id: video.id, _sum: { rating: video.rating } }); - - group = await db.ratedVideo.groupBy({ - by: ['id'], - _sum: { rating: true }, - having: { rating: { _sum: { gt: video.rating } } }, - }); - expect(group).toHaveLength(0); - - expect(() => db.ratedVideo.groupBy({ by: 'viewCount' })).toThrow( - 'groupBy with fields from base type is not supported yet' - ); - expect(() => db.ratedVideo.groupBy({ having: { rating: { gt: 0 }, viewCount: { gt: 0 } } })).toThrow( - 'groupBy with fields from base type is not supported yet' - ); - }); - - it('many to many', async () => { - const { enhance } = await loadSchema(POLYMORPHIC_MANY_TO_MANY_SCHEMA); - const db = enhance(); - - const video = await db.video.create({ data: { viewCount: 1, duration: 100 } }); - const image = await db.image.create({ data: { viewCount: 2, format: 'png' } }); - - await expect( - db.user.create({ - data: { - id: 1, - level: 10, - assets: { - connect: [{ id: video.id }, { id: image.id }], - }, - }, - include: { assets: true }, - }) - ).resolves.toMatchObject({ - id: 1, - level: 10, - assets: expect.arrayContaining([video, image]), - }); - - await expect(db.user.findUnique({ where: { id: 1 }, include: { assets: true } })).resolves.toMatchObject({ - id: 1, - assets: expect.arrayContaining([video, image]), - }); - await expect(db.asset.findUnique({ where: { id: video.id }, include: { users: true } })).resolves.toMatchObject( - { - id: video.id, - users: expect.arrayContaining([{ id: 1, level: 10 }]), - } - ); - }); - - it('handles very long concrete model name', async () => { - const { db, user } = await setup(); - - await db.veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA.create({ - data: { - owner: { connect: { id: user.id } }, - duration: 62, - url: 'https://whatever.com/example.mp4', - propA: 'propA', - }, - }); - - await db.veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB.create({ - data: { - owner: { connect: { id: user.id } }, - duration: 62, - url: 'https://whatever.com/example.mp4', - propB: 'propB', - }, - }); - - const foundUser = await db.user.findFirst({ - where: { id: user.id }, - include: { - assets: true, - }, - }); - - expect(foundUser).toEqual( - expect.objectContaining({ - assets: expect.arrayContaining([ - expect.objectContaining({ - videoType: 'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA', - propA: 'propA', - }), - expect.objectContaining({ - videoType: 'VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB', - propB: 'propB', - }), - ]), - }) - ); - }); - - it('typescript compilation plain prisma', async () => { - const src = ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - - const prisma = new PrismaClient(); - - async function main() { - const db = enhance(prisma); - - const user1 = await db.user.create({ data: { } }); - - await db.ratedVideo.create({ - data: { - owner: { connect: { id: user1.id } }, - duration: 100, - url: 'abc', - rating: 10, - }, - }); - - await db.image.create({ - data: { - owner: { connect: { id: user1.id } }, - format: 'webp', - }, - }); - - const video = await db.video.findFirst({ include: { owner: true } }); - console.log(video?.duration); - console.log(video?.viewCount); - - const asset = await db.asset.findFirstOrThrow(); - console.log(asset.assetType); - console.log(asset.viewCount); - - if (asset.assetType === 'Video') { - console.log('Video: duration', asset.duration); - } else { - console.log('Image: format', asset.format); - } - } - - main(); - `; - await loadSchema(schema, { - compile: true, - enhancements: ['delegate'], - extraSourceFiles: [ - { - name: 'main.ts', - content: src, - }, - ], - }); - }); - - it('typescript compilation extended prisma', async () => { - const src = ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - - const prisma = new PrismaClient().$extends({ - model: { - user: { - async signUp() { - return prisma.user.create({ data: {} }); - }, - }, - }, - }); - - async function main() { - const db = enhance(prisma); - - const user1 = await db.user.signUp(); - - await db.ratedVideo.create({ - data: { - owner: { connect: { id: user1.id } }, - duration: 100, - url: 'abc', - rating: 10, - }, - }); - - await db.image.create({ - data: { - owner: { connect: { id: user1.id } }, - format: 'webp', - }, - }); - - const video = await db.video.findFirst({ include: { owner: true } }); - console.log(video?.duration); - console.log(video?.viewCount); - - const asset = await db.asset.findFirstOrThrow(); - console.log(asset.assetType); - console.log(asset.viewCount); - - if (asset.assetType === 'Video') { - console.log('Video: duration', asset.duration); - } else { - console.log('Image: format', asset.format); - } - } - - main(); - `; - await loadSchema(schema, { - compile: true, - enhancements: ['delegate'], - extraSourceFiles: [ - { - name: 'main.ts', - content: src, - }, - ], - }); - }); - - it('merges hierarchy correctly', async () => { - const { enhance } = await loadSchema( - ` - model Asset { - id Int @id @default(autoincrement()) - type String - viewCount Int - comments Comment[] - @@delegate(type) - } - - model Post extends Asset { - title String - } - - model Comment { - id Int @id @default(autoincrement()) - type String - asset Asset @relation(fields: [assetId], references: [id]) - assetId Int - moderated Boolean - @@delegate(type) - } - - model TextComment extends Comment { - text String - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const post = await db.post.create({ data: { title: 'Post1', viewCount: 1 } }); - const comment = await db.textComment.create({ - data: { text: 'Comment1', moderated: true, asset: { connect: { id: post.id } } }, - }); - - // delegate include delegate - let r = await db.asset.findFirst({ include: { comments: true } }); - expect(r).toMatchObject({ viewCount: 1, comments: [comment] }); - - // concrete include delegate - r = await db.post.findFirst({ include: { comments: true } }); - expect(r).toMatchObject({ ...post, comments: [comment] }); - }); - - it('works with one-to-one self relation', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - successorId Int? @unique - successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id]) - predecessor User? @relation("BlogOwnerHistory") - type String - @@delegate(type) - } - - model Person extends User { - } - - model Organization extends User { - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const u1 = await db.person.create({ data: {} }); - const u2 = await db.organization.create({ - data: { predecessor: { connect: { id: u1.id } } }, - include: { predecessor: true }, - }); - expect(u2).toMatchObject({ id: u2.id, predecessor: { id: u1.id } }); - const foundP1 = await db.person.findUnique({ where: { id: u1.id }, include: { successor: true } }); - expect(foundP1).toMatchObject({ id: u1.id, successor: { id: u2.id } }); - }); - - it('works with one-to-many self relation', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - name String? - parentId Int? - parent User? @relation("ParentChild", fields: [parentId], references: [id]) - children User[] @relation("ParentChild") - type String - @@delegate(type) - } - - model Person extends User { - } - - model Organization extends User { - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const u1 = await db.person.create({ data: {} }); - const u2 = await db.organization.create({ - data: { parent: { connect: { id: u1.id } } }, - include: { parent: true }, - }); - expect(u2).toMatchObject({ id: u2.id, parent: { id: u1.id } }); - const foundP1 = await db.person.findUnique({ where: { id: u1.id }, include: { children: true } }); - expect(foundP1).toMatchObject({ id: u1.id, children: [{ id: u2.id }] }); - }); - - it('works with many-to-many self relation', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - name String? - followedBy User[] @relation("UserFollows") - following User[] @relation("UserFollows") - type String - @@delegate(type) - } - - model Person extends User { - } - - model Organization extends User { - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const u1 = await db.person.create({ data: {} }); - const u2 = await db.organization.create({ - data: { following: { connect: { id: u1.id } } }, - include: { following: true }, - }); - expect(u2).toMatchObject({ id: u2.id, following: [{ id: u1.id }] }); - const foundP1 = await db.person.findUnique({ where: { id: u1.id }, include: { followedBy: true } }); - expect(foundP1).toMatchObject({ id: u1.id, followedBy: [{ id: u2.id }] }); - }); -}); diff --git a/tests/integration/tests/enhancements/with-delegate/omit-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/omit-interaction.test.ts deleted file mode 100644 index 7b4be2309..000000000 --- a/tests/integration/tests/enhancements/with-delegate/omit-interaction.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Polymorphic @omit', () => { - const model = ` - model User { - id Int @id @default(autoincrement()) - assets Asset[] - - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - type String - foo String @omit - user User? @relation(fields: [userId], references: [id]) - userId Int? @unique - - @@delegate(type) - @@allow('all', true) - } - - model Post extends Asset { - title String - bar String @omit - } - `; - - it('omits when queried via a concrete model', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - const post = await db.post.create({ data: { foo: 'foo', bar: 'bar', title: 'Post1' } }); - expect(post.foo).toBeUndefined(); - expect(post.bar).toBeUndefined(); - - const foundPost = await db.post.findUnique({ where: { id: post.id } }); - expect(foundPost.foo).toBeUndefined(); - expect(foundPost.bar).toBeUndefined(); - }); - - it('omits when queried via a base model', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - const post = await db.post.create({ data: { foo: 'foo', bar: 'bar', title: 'Post1' } }); - expect(post.foo).toBeUndefined(); - expect(post.bar).toBeUndefined(); - - const foundAsset = await db.asset.findUnique({ where: { id: post.id } }); - expect(foundAsset.foo).toBeUndefined(); - expect(foundAsset.bar).toBeUndefined(); - expect(foundAsset.title).toBeTruthy(); - }); - - it('omits when discriminator is not selected', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - const post = await db.post.create({ - data: { foo: 'foo', bar: 'bar', title: 'Post1' }, - }); - expect(post.foo).toBeUndefined(); - expect(post.bar).toBeUndefined(); - - const foundAsset = await db.asset.findUnique({ - where: { id: post.id }, - select: { id: true, foo: true }, - }); - console.log(foundAsset); - expect(foundAsset.foo).toBeUndefined(); - expect(foundAsset.bar).toBeUndefined(); - }); - - it('omits when queried in a nested context', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - const user = await db.user.create({ data: {} }); - const post = await db.post.create({ - data: { foo: 'foo', bar: 'bar', title: 'Post1', user: { connect: { id: user.id } } }, - }); - expect(post.foo).toBeUndefined(); - expect(post.bar).toBeUndefined(); - - const foundUser = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); - expect(foundUser.assets[0].foo).toBeUndefined(); - expect(foundUser.assets[0].bar).toBeUndefined(); - expect(foundUser.assets[0].title).toBeTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-delegate/password-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/password-interaction.test.ts deleted file mode 100644 index 3b05a3937..000000000 --- a/tests/integration/tests/enhancements/with-delegate/password-interaction.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import { compareSync } from 'bcryptjs'; - -describe('Polymorphic @omit', () => { - const model = ` - model User { - id Int @id @default(autoincrement()) - posts Post[] - - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - type String - assetPassword String @password - - @@delegate(type) - @@allow('all', true) - } - - model Post extends Asset { - title String - postPassword String @password - user User? @relation(fields: [userId], references: [id]) - userId Int? @unique - } - `; - - it('hashes when created directly', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - const post = await db.post.create({ data: { title: 'Post1', assetPassword: 'asset', postPassword: 'post' } }); - expect(compareSync('asset', post.assetPassword)).toBeTruthy(); - expect(compareSync('post', post.postPassword)).toBeTruthy(); - }); - - it('hashes when created nested', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - const user = await db.user.create({ - data: { posts: { create: { title: 'Post1', assetPassword: 'asset', postPassword: 'post' } } }, - include: { posts: true }, - }); - expect(compareSync('asset', user.posts[0].assetPassword)).toBeTruthy(); - expect(compareSync('post', user.posts[0].postPassword)).toBeTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts deleted file mode 100644 index 0203b2991..000000000 --- a/tests/integration/tests/enhancements/with-delegate/plugin-interaction.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import { POLYMORPHIC_SCHEMA } from './utils'; -import path from 'path'; - -describe('Polymorphic Plugin Interaction Test', () => { - it('tanstack-query', async () => { - const tanstackPlugin = path.resolve(__dirname, '../../../../../packages/plugins/tanstack-query/dist'); - const schema = ` - ${POLYMORPHIC_SCHEMA} - - plugin hooks { - provider = '${tanstackPlugin}' - output = '$projectRoot/hooks' - target = 'react' - version = 'v5' - } - `; - - await loadSchema(schema, { - compile: true, - copyDependencies: [tanstackPlugin], - extraDependencies: ['@tanstack/react-query@5.56.x'], - }); - }); - - it('swr', async () => { - const schema = ` - ${POLYMORPHIC_SCHEMA} - - plugin hooks { - provider = '@zenstackhq/swr' - output = '$projectRoot/hooks' - } - `; - - await loadSchema(schema, { - compile: true, - extraDependencies: ['swr'], - }); - }); - - it('trpc', async () => { - const schema = ` - ${POLYMORPHIC_SCHEMA} - - plugin trpc { - provider = '@zenstackhq/trpc' - output = '$projectRoot/routers' - generateClientHelpers = 'react' - } - `; - - await loadSchema(schema, { - compile: true, - extraDependencies: ['@trpc/client@10', '@trpc/server@10', '@trpc/react-query@10'], - }); - }); - - it('zod', async () => { - const { zodSchemas } = await loadSchema(POLYMORPHIC_SCHEMA, { fullZod: true }); - - // model schema - expect( - zodSchemas.models.AssetSchema.parse({ - id: 1, - assetType: 'video', - createdAt: new Date(), - updatedAt: new Date(), - viewCount: 100, - }) - ).toBeTruthy(); - - expect( - zodSchemas.models.AssetSchema.parse({ - id: 1, - assetType: 'video', - createdAt: new Date(), - updatedAt: new Date(), - viewCount: 100, - videoType: 'ratedVideo', // should be stripped - }).videoType - ).toBeUndefined(); - - expect( - zodSchemas.models.VideoSchema.parse({ - id: 1, - assetType: 'video', - videoType: 'ratedVideo', - duration: 100, - url: 'http://example.com', - createdAt: new Date(), - updatedAt: new Date(), - viewCount: 100, - }) - ).toBeTruthy(); - - expect(() => - zodSchemas.models.VideoSchema.parse({ - id: 1, - assetType: 'video', - videoType: 'ratedVideo', - url: 'http://example.com', - createdAt: new Date(), - updatedAt: new Date(), - viewCount: 100, - }) - ).toThrow('duration'); - - // create schema - expect( - zodSchemas.models.VideoCreateSchema.parse({ - duration: 100, - url: 'http://example.com', - }).assetType // discriminator should not be set - ).toBeUndefined(); - - // update schema - expect( - zodSchemas.models.VideoUpdateSchema.parse({ - duration: 100, - url: 'http://example.com', - }).assetType // discriminator should not be set - ).toBeUndefined(); - - // prisma create schema - expect( - zodSchemas.models.VideoPrismaCreateSchema.strip().parse({ - assetType: 'video', - }).assetType // discriminator should not be set - ).toBeUndefined(); - - // input object schema - expect( - zodSchemas.objects.RatedVideoCreateInputObjectSchema.parse({ - duration: 100, - viewCount: 200, - url: 'http://www.example.com', - rating: 5, - }) - ).toBeTruthy(); - - expect(() => - zodSchemas.objects.RatedVideoCreateInputObjectSchema.parse({ - duration: 100, - viewCount: 200, - url: 'http://www.example.com', - rating: 5, - videoType: 'ratedVideo', - }) - ).toThrow('videoType'); - }); -}); diff --git a/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts deleted file mode 100644 index 67fc456af..000000000 --- a/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts +++ /dev/null @@ -1,654 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Polymorphic Policy Test', () => { - it('simple boolean', async () => { - const booleanCondition = ` - model User { - id Int @id @default(autoincrement()) - level Int @default(0) - assets Asset[] - banned Boolean @default(false) - - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - published Boolean @default(false) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - assetType String - viewCount Int @default(0) - - @@delegate(assetType) - @@allow('create', viewCount >= 0) - @@deny('read', !published) - @@allow('read', true) - @@deny('all', owner.banned) - } - - model Video extends Asset { - watched Boolean @default(false) - videoType String - - @@delegate(videoType) - @@deny('read', !watched) - @@allow('read', true) - } - - model RatedVideo extends Video { - rated Boolean @default(false) - @@deny('read', !rated) - @@allow('read', true) - } - `; - - const booleanExpression = ` - model User { - id Int @id @default(autoincrement()) - level Int @default(0) - assets Asset[] - banned Boolean @default(false) - - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - published Boolean @default(false) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - assetType String - viewCount Int @default(0) - - @@delegate(assetType) - @@allow('create', viewCount >= 0) - @@deny('read', published == false) - @@allow('read', true) - @@deny('all', owner.banned == true) - } - - model Video extends Asset { - watched Boolean @default(false) - videoType String - - @@delegate(videoType) - @@deny('read', watched == false) - @@allow('read', true) - } - - model RatedVideo extends Video { - rated Boolean @default(false) - @@deny('read', rated == false) - @@allow('read', true) - } - `; - - for (const schema of [booleanCondition, booleanExpression]) { - const { enhanceRaw: enhance, prisma } = await loadSchema(schema); - - const fullDb = enhance(prisma, undefined, { kinds: ['delegate'] }); - - const user = await fullDb.user.create({ data: { id: 1 } }); - const userDb = enhance(prisma, { user: { id: user.id } }, { kinds: ['delegate', 'policy'] }); - - // violating Asset create - await expect( - userDb.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, viewCount: -1 }, - }) - ).toBeRejectedByPolicy(); - - let video = await fullDb.ratedVideo.create({ - data: { owner: { connect: { id: user.id } } }, - }); - // violating all three layer read - await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveNull(); - await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveNull(); - await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveNull(); - - video = await fullDb.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, published: true }, - }); - // violating Video && RatedVideo read - await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveTruthy(); - await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveNull(); - await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveNull(); - - video = await fullDb.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, published: true, watched: true }, - }); - // violating RatedVideo read - await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveTruthy(); - await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveTruthy(); - await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveNull(); - - video = await fullDb.ratedVideo.create({ - data: { owner: { connect: { id: user.id } }, rated: true, watched: true, published: true }, - }); - // meeting all read conditions - await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveTruthy(); - await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveTruthy(); - await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveTruthy(); - - // ban the user - await prisma.user.update({ where: { id: user.id }, data: { banned: true } }); - - // banned user can't read - await expect(userDb.asset.findUnique({ where: { id: video.id } })).toResolveNull(); - await expect(userDb.video.findUnique({ where: { id: video.id } })).toResolveNull(); - await expect(userDb.ratedVideo.findUnique({ where: { id: video.id } })).toResolveNull(); - - // banned user can't create - await expect( - userDb.ratedVideo.create({ - data: { owner: { connect: { id: user.id } } }, - }) - ).toBeRejectedByPolicy(); - } - }); - - it('interaction with updateMany/deleteMany', async () => { - const schema = ` - model User { - id Int @id @default(autoincrement()) - level Int @default(0) - assets Asset[] - banned Boolean @default(false) - - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - published Boolean @default(false) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - assetType String - viewCount Int @default(0) - version Int @default(0) - - @@delegate(assetType) - @@deny('update', viewCount > 0) - @@deny('delete', viewCount > 0) - @@allow('all', true) - } - - model Video extends Asset { - watched Boolean @default(false) - - @@deny('update', watched) - @@deny('delete', watched) - } - `; - - const { enhance } = await loadSchema(schema); - const db = enhance(); - - const user = await db.user.create({ data: { id: 1 } }); - const vid1 = await db.video.create({ - data: { watched: false, viewCount: 0, owner: { connect: { id: user.id } } }, - }); - const vid2 = await db.video.create({ - data: { watched: true, viewCount: 1, owner: { connect: { id: user.id } } }, - }); - - await expect(db.asset.updateMany({ data: { version: { increment: 1 } } })).resolves.toMatchObject({ - count: 1, - }); - await expect(db.asset.findUnique({ where: { id: vid1.id } })).resolves.toMatchObject({ version: 1 }); - await expect(db.asset.findUnique({ where: { id: vid2.id } })).resolves.toMatchObject({ version: 0 }); - - await expect(db.asset.deleteMany()).resolves.toMatchObject({ - count: 1, - }); - await expect(db.asset.findUnique({ where: { id: vid1.id } })).toResolveNull(); - await expect(db.asset.findUnique({ where: { id: vid2.id } })).toResolveTruthy(); - }); - - it('interaction with connect', async () => { - const schema = ` - model User { - id Int @id @default(autoincrement()) - } - - model Asset { - id Int @id @default(autoincrement()) - assetType String - comments Comment[] - - @@delegate(assetType) - @@allow('all', true) - } - - model Post extends Asset { - title String - postType String - @@delegate(postType) - } - - model RatedPost extends Post { - rating Int - } - - model Comment { - id Int @id @default(autoincrement()) - content String - asset Asset? @relation(fields: [assetId], references: [id]) - assetId Int? - @@allow('read,create', true) - @@allow('update', auth().id == 1) - } - `; - - const { enhance } = await loadSchema(schema); - const db = enhance(); - - const comment1 = await db.comment.create({ - data: { content: 'Comment1' }, - }); - - await expect( - db.ratedPost.create({ data: { title: 'Post1', rating: 5, comments: { connect: { id: comment1.id } } } }) - ).toBeRejectedByPolicy(); - - const post1 = await db.ratedPost.create({ data: { title: 'Post1', rating: 5 } }); - await expect( - db.post.update({ where: { id: post1.id }, data: { comments: { connect: { id: comment1.id } } } }) - ).toBeRejectedByPolicy(); - await expect( - db.ratedPost.update({ where: { id: post1.id }, data: { comments: { connect: { id: comment1.id } } } }) - ).toBeRejectedByPolicy(); - - const user1Db = enhance({ id: 1 }); - await expect( - user1Db.ratedPost.update({ where: { id: post1.id }, data: { comments: { connect: { id: comment1.id } } } }) - ).toResolveTruthy(); - }); - - it('respects base model policies when queried from a sub', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - assets Asset[] - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - deleted Boolean @default(false) - user User @relation(fields: [userId], references: [id]) - userId Int - type String - @@delegate(type) - @@allow('all', true) - @@deny('read', deleted) - } - - model Post extends Asset { - title String - } - ` - ); - - const db = enhance(); - const user = await db.user.create({ data: { id: 1 } }); - const post = await db.post.create({ data: { id: 1, title: 'Post1', userId: user.id } }); - - await expect(db.post.findUnique({ where: { id: post.id } })).toResolveTruthy(); - await expect(db.asset.findUnique({ where: { id: post.id } })).toResolveTruthy(); - let withAssets = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); - expect(withAssets.assets).toHaveLength(1); - - await prisma.asset.update({ where: { id: post.id }, data: { deleted: true } }); - await expect(db.post.findUnique({ where: { id: post.id } })).toResolveFalsy(); - await expect(db.asset.findUnique({ where: { id: post.id } })).toResolveFalsy(); - withAssets = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); - expect(withAssets.assets).toHaveLength(0); - - // unable to read back - await expect( - db.post.create({ data: { title: 'Post2', deleted: true, userId: user.id } }) - ).toBeRejectedByPolicy(); - // actually created - await expect(prisma.post.count()).resolves.toBe(2); - - // unable to read back - await expect(db.post.update({ where: { id: 2 }, data: { title: 'Post2-1' } })).toBeRejectedByPolicy(); - // actually updated - await expect(prisma.post.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ title: 'Post2-1' }); - - // unable to read back - await expect(db.post.delete({ where: { id: 2 } })).toBeRejectedByPolicy(); - // actually deleted - await expect(prisma.post.findUnique({ where: { id: 2 } })).toResolveFalsy(); - }); - - it('respects sub model policies when queried from a base: case 1', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - assets Asset[] - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - value Int @default(0) - deleted Boolean @default(false) - type String - @@delegate(type) - @@allow('all', true) - } - - model Post extends Asset { - title String - @@deny('read', deleted) - } - ` - ); - - const db = enhance(); - const user = await db.user.create({ data: { id: 1 } }); - - // create read back - const post = await db.post.create({ data: { id: 1, title: 'Post1', userId: user.id } }); - expect(post.type).toBe('Post'); - expect(post.title).toBe('Post1'); - expect(post.value).toBe(0); - - // update read back - const updatedPost = await db.post.update({ where: { id: post.id }, data: { value: 1 } }); - expect(updatedPost.type).toBe('Post'); - expect(updatedPost.title).toBe('Post1'); - expect(updatedPost.value).toBe(1); - - // both asset and post fields are readable - const readPost = await db.post.findUnique({ where: { id: post.id } }); - expect(readPost.title).toBe('Post1'); - - const readAsset = await db.asset.findUnique({ where: { id: post.id } }); - expect(readAsset.type).toBe('Post'); - const userWithAssets = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); - expect(userWithAssets.assets[0].title).toBe('Post1'); - - await prisma.asset.update({ where: { id: post.id }, data: { deleted: true } }); - - // asset fields are readable, but not post fields - const readAsset1 = await db.asset.findUnique({ where: { id: post.id } }); - expect(readAsset1.type).toBe('Post'); - expect(readAsset1.title).toBeUndefined(); - - const userWithAssets1 = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); - expect(userWithAssets1.assets[0].type).toBe('Post'); - expect(userWithAssets1.assets[0].title).toBeUndefined(); - - // update read back - const updateRead = await db.asset.update({ where: { id: post.id }, data: { value: 2 } }); - expect(updateRead.value).toBe(2); - // cannot read back sub model - expect(updateRead.title).toBeUndefined(); - - // delete read back - const deleteRead = await db.asset.delete({ where: { id: post.id } }); - expect(deleteRead.value).toBe(2); - // cannot read back sub model - expect(deleteRead.title).toBeUndefined(); - // actually deleted - await expect(prisma.asset.findUnique({ where: { id: post.id } })).toResolveFalsy(); - await expect(prisma.post.findUnique({ where: { id: post.id } })).toResolveFalsy(); - }); - - it('respects sub model policies when queried from a base: case 2', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - assets Asset[] - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - value Int @default(0) - type String - @@delegate(type) - @@allow('all', true) - } - - model Post extends Asset { - title String - deleted Boolean @default(false) - @@deny('read', deleted) - } - ` - ); - - const db = enhance(); - const user = await db.user.create({ data: { id: 1 } }); - - // create read back - const post = await db.post.create({ data: { id: 1, title: 'Post1', userId: user.id } }); - expect(post.type).toBe('Post'); - expect(post.title).toBe('Post1'); - expect(post.value).toBe(0); - - // update read back - const updatedPost = await db.post.update({ where: { id: post.id }, data: { value: 1 } }); - expect(updatedPost.type).toBe('Post'); - expect(updatedPost.title).toBe('Post1'); - expect(updatedPost.value).toBe(1); - - // both asset and post fields are readable - const readPost = await db.post.findUnique({ where: { id: post.id } }); - expect(readPost.title).toBe('Post1'); - - const readAsset = await db.asset.findUnique({ where: { id: post.id } }); - expect(readAsset.type).toBe('Post'); - const userWithAssets = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); - expect(userWithAssets.assets[0].title).toBe('Post1'); - - await prisma.post.update({ where: { id: post.id }, data: { deleted: true } }); - - // asset fields are readable, but not post fields - const readAsset1 = await db.asset.findUnique({ where: { id: post.id } }); - expect(readAsset1.type).toBe('Post'); - expect(readAsset1.title).toBeUndefined(); - - const userWithAssets1 = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); - expect(userWithAssets1.assets[0].type).toBe('Post'); - expect(userWithAssets1.assets[0].title).toBeUndefined(); - - // update read back - const updateRead = await db.asset.update({ where: { id: post.id }, data: { value: 2 } }); - expect(updateRead.value).toBe(2); - // cannot read back sub model - expect(updateRead.title).toBeUndefined(); - - // delete read back - const deleteRead = await db.asset.delete({ where: { id: post.id } }); - expect(deleteRead.value).toBe(2); - // cannot read back sub model - expect(deleteRead.title).toBeUndefined(); - // actually deleted - await expect(prisma.asset.findUnique({ where: { id: post.id } })).toResolveFalsy(); - await expect(prisma.post.findUnique({ where: { id: post.id } })).toResolveFalsy(); - }); - - it('respects sub model policies when queried from a base: case 3', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - assets Asset[] - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - value Int @default(0) - type String - @@delegate(type) - @@allow('all', value > 0) - } - - model Post extends Asset { - title String - deleted Boolean @default(false) - @@deny('read', deleted) - } - ` - ); - - const db = enhance(); - const user = await db.user.create({ data: { id: 1 } }); - - // can't create - await expect( - db.post.create({ data: { id: 1, title: 'Post1', userId: user.id, value: 0 } }) - ).toBeRejectedByPolicy(); - - // can't read back - await expect( - db.post.create({ data: { id: 1, title: 'Post1', userId: user.id, value: 1, deleted: true } }) - ).toBeRejectedByPolicy(); - - await expect( - db.post.create({ data: { id: 2, title: 'Post1', userId: user.id, value: 1, deleted: false } }) - ).toResolveTruthy(); - - await expect(db.asset.findMany()).resolves.toHaveLength(2); - }); - - it('respects field-level policies', async () => { - const { enhance } = await loadSchema(` - model User { - id Int @id @default(autoincrement()) - } - - model Asset { - id Int @id @default(autoincrement()) - type String - foo String @allow('read', auth().id == 1) - - @@delegate(type) - @@allow('all', true) - } - - model Post extends Asset { - title String - bar String @deny('read', auth().id != 1) - } - `); - - const db = enhance({ id: 1 }); - const post = await db.post.create({ data: { foo: 'foo', bar: 'bar', title: 'Post1' } }); - expect(post.foo).toBeTruthy(); - expect(post.bar).toBeTruthy(); - - const foundPost = await db.post.findUnique({ where: { id: post.id } }); - expect(foundPost.foo).toBeTruthy(); - expect(foundPost.bar).toBeTruthy(); - - const db2 = enhance({ id: 2 }); - const post2 = await db2.post.create({ data: { foo: 'foo', bar: 'bar', title: 'Post2' } }); - expect(post2.title).toBeTruthy(); - expect(post2.foo).toBeUndefined(); - expect(post2.bar).toBeUndefined(); - - const foundPost2 = await db2.post.findUnique({ where: { id: post2.id } }); - expect(foundPost2.foo).toBeUndefined(); - expect(foundPost2.bar).toBeUndefined(); - }); - - it('respects concrete policies when read as base optional relation', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - asset Asset? - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - type String - - @@delegate(type) - @@allow('all', true) - } - - model Post extends Asset { - title String - private Boolean - @@allow('create', true) - @@deny('read', private) - } - ` - ); - - const fullDb = enhance(undefined, { kinds: ['delegate'] }); - await fullDb.user.create({ data: { id: 1 } }); - await fullDb.post.create({ data: { title: 'Post1', private: true, user: { connect: { id: 1 } } } }); - await expect(fullDb.user.findUnique({ where: { id: 1 }, include: { asset: true } })).resolves.toMatchObject({ - asset: expect.objectContaining({ type: 'Post' }), - }); - - const db = enhance(); - const read = await db.user.findUnique({ where: { id: 1 }, include: { asset: true } }); - expect(read.asset).toBeTruthy(); - expect(read.asset.title).toBeUndefined(); - }); - - it('respects concrete policies when read as base required relation', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - asset Asset @relation(fields: [assetId], references: [id]) - assetId Int @unique - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - user User? - type String - - @@delegate(type) - @@allow('all', true) - } - - model Post extends Asset { - title String - private Boolean - @@deny('read', private) - } - ` - ); - - const fullDb = enhance(undefined, { kinds: ['delegate'] }); - await fullDb.post.create({ data: { id: 1, title: 'Post1', private: true, user: { create: { id: 1 } } } }); - await expect(fullDb.user.findUnique({ where: { id: 1 }, include: { asset: true } })).resolves.toMatchObject({ - asset: expect.objectContaining({ type: 'Post' }), - }); - - const db = enhance(); - const read = await db.user.findUnique({ where: { id: 1 }, include: { asset: true } }); - expect(read).toBeTruthy(); - expect(read.asset.title).toBeUndefined(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-delegate/utils.ts b/tests/integration/tests/enhancements/with-delegate/utils.ts deleted file mode 100644 index 23bae33db..000000000 --- a/tests/integration/tests/enhancements/with-delegate/utils.ts +++ /dev/null @@ -1,85 +0,0 @@ -export const POLYMORPHIC_SCHEMA = ` -model User { - id Int @id @default(autoincrement()) - email String? @unique - level Int @default(0) - assets Asset[] - ratedVideos RatedVideo[] @relation('direct') - - @@allow('all', true) -} - -model Asset { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - viewCount Int @default(0) - owner User? @relation(fields: [ownerId], references: [id]) - ownerId Int? - assetType String - - @@delegate(assetType) - @@allow('all', true) -} - -model Video extends Asset { - duration Int - url String - videoType String - - @@delegate(videoType) -} - -model RatedVideo extends Video { - rating Int - user User? @relation(name: 'direct', fields: [userId], references: [id]) - userId Int? -} - -model Image extends Asset { - format String - gallery Gallery? @relation(fields: [galleryId], references: [id]) - galleryId Int? -} - -model Gallery { - id Int @id @default(autoincrement()) - images Image[] -} - -model VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameA extends Video { - propA String -} - -model VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongModelNameB extends Video { - propB String -} -`; - -export const POLYMORPHIC_MANY_TO_MANY_SCHEMA = ` -model User { - id Int @id @default(autoincrement()) - level Int @default(0) - assets Asset[] - - @@allow('all', true) -} - -model Asset { - id Int @id @default(autoincrement()) - viewCount Int @default(0) - users User[] - assetType String - - @@delegate(assetType) - @@allow('all', true) -} - -model Video extends Asset { - duration Int -} - -model Image extends Asset { - format String -} -`; diff --git a/tests/integration/tests/enhancements/with-delegate/validation.test.ts b/tests/integration/tests/enhancements/with-delegate/validation.test.ts deleted file mode 100644 index 8e729c337..000000000 --- a/tests/integration/tests/enhancements/with-delegate/validation.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('Polymorphic input validation', () => { - it('rejects aux fields in mutation', async () => { - const { enhance } = await loadSchema( - ` - model Asset { - id Int @id @default(autoincrement()) - type String - - @@delegate(type) - @@allow('all', true) - } - - model Post extends Asset { - title String - } - ` - ); - - const db = enhance(); - const asset = await db.post.create({ data: { title: 'Post1' } }); - await expect( - db.asset.update({ where: { id: asset.id }, data: { delegate_aux_post: { update: { title: 'Post2' } } } }) - ).rejects.toThrow('Auxiliary relation field "delegate_aux_post" cannot be set directly'); - }); -}); diff --git a/tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts b/tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts deleted file mode 100644 index 71d32769f..000000000 --- a/tests/integration/tests/enhancements/with-encrypted/with-encrypted.test.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { FieldInfo } from '@zenstackhq/runtime'; -import { loadSchema, loadModelWithError } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('Encrypted test', () => { - let origDir: string; - const encryptionKey = new Uint8Array(Buffer.from('AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=', 'base64')); - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(async () => { - process.chdir(origDir); - }); - - it('Simple encryption test', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - encrypted_value String @encrypted() - }`, - { - enhancements: ['encryption'], - enhanceOptions: { - encryption: { encryptionKey }, - }, - } - ); - - const sudoDb = enhance(undefined, { kinds: [] }); - - const db = enhance(); - - const create = await db.user.create({ - data: { - id: '1', - encrypted_value: 'abc123', - }, - }); - - const read = await db.user.findUnique({ - where: { - id: '1', - }, - }); - - const sudoRead = await sudoDb.user.findUnique({ - where: { - id: '1', - }, - }); - - const rawRead = await prisma.user.findUnique({ where: { id: '1' } }); - - expect(create.encrypted_value).toBe('abc123'); - expect(read.encrypted_value).toBe('abc123'); - expect(sudoRead.encrypted_value).not.toBe('abc123'); - expect(rawRead.encrypted_value).not.toBe('abc123'); - - // update - const updated = await db.user.update({ - where: { id: '1' }, - data: { encrypted_value: 'abc234' }, - }); - expect(updated.encrypted_value).toBe('abc234'); - await expect(db.user.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ - encrypted_value: 'abc234', - }); - await expect(prisma.user.findUnique({ where: { id: '1' } })).resolves.not.toMatchObject({ - encrypted_value: 'abc234', - }); - - // upsert with create - const upsertCreate = await db.user.upsert({ - where: { id: '2' }, - create: { - id: '2', - encrypted_value: 'abc345', - }, - update: { - encrypted_value: 'abc456', - }, - }); - expect(upsertCreate.encrypted_value).toBe('abc345'); - await expect(db.user.findUnique({ where: { id: '2' } })).resolves.toMatchObject({ - encrypted_value: 'abc345', - }); - await expect(prisma.user.findUnique({ where: { id: '2' } })).resolves.not.toMatchObject({ - encrypted_value: 'abc345', - }); - - // upsert with update - const upsertUpdate = await db.user.upsert({ - where: { id: '2' }, - create: { - id: '2', - encrypted_value: 'abc345', - }, - update: { - encrypted_value: 'abc456', - }, - }); - expect(upsertUpdate.encrypted_value).toBe('abc456'); - await expect(db.user.findUnique({ where: { id: '2' } })).resolves.toMatchObject({ - encrypted_value: 'abc456', - }); - await expect(prisma.user.findUnique({ where: { id: '2' } })).resolves.not.toMatchObject({ - encrypted_value: 'abc456', - }); - - // createMany - await db.user.createMany({ - data: [ - { id: '3', encrypted_value: 'abc567' }, - { id: '4', encrypted_value: 'abc678' }, - ], - }); - await expect(db.user.findUnique({ where: { id: '3' } })).resolves.toMatchObject({ - encrypted_value: 'abc567', - }); - await expect(prisma.user.findUnique({ where: { id: '3' } })).resolves.not.toMatchObject({ - encrypted_value: 'abc567', - }); - - // createManyAndReturn - await expect( - db.user.createManyAndReturn({ - data: [ - { id: '5', encrypted_value: 'abc789' }, - { id: '6', encrypted_value: 'abc890' }, - ], - }) - ).resolves.toEqual( - expect.arrayContaining([ - { id: '5', encrypted_value: 'abc789' }, - { id: '6', encrypted_value: 'abc890' }, - ]) - ); - await expect(db.user.findUnique({ where: { id: '5' } })).resolves.toMatchObject({ - encrypted_value: 'abc789', - }); - await expect(prisma.user.findUnique({ where: { id: '5' } })).resolves.not.toMatchObject({ - encrypted_value: 'abc789', - }); - }); - - it('Works with nullish values', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - encrypted_value String? @encrypted() - }`, - { - enhancements: ['encryption'], - enhanceOptions: { - encryption: { encryptionKey }, - }, - } - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: '1', encrypted_value: '' } })).resolves.toMatchObject({ - encrypted_value: '', - }); - await expect(prisma.user.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ encrypted_value: '' }); - - await expect(db.user.create({ data: { id: '2' } })).resolves.toMatchObject({ - encrypted_value: null, - }); - await expect(prisma.user.findUnique({ where: { id: '2' } })).resolves.toMatchObject({ encrypted_value: null }); - - await expect(db.user.create({ data: { id: '3', encrypted_value: null } })).resolves.toMatchObject({ - encrypted_value: null, - }); - await expect(prisma.user.findUnique({ where: { id: '3' } })).resolves.toMatchObject({ encrypted_value: null }); - }); - - it('Decrypts nested fields', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - posts Post[] - } - - model Post { - id String @id @default(cuid()) - title String @encrypted() - author User @relation(fields: [authorId], references: [id]) - authorId String - } - `, - { - enhancements: ['encryption'], - enhanceOptions: { - encryption: { encryptionKey }, - }, - } - ); - - const db = enhance(); - - const create = await db.user.create({ - data: { - id: '1', - posts: { create: { title: 'Post1' } }, - }, - include: { posts: true }, - }); - expect(create.posts[0].title).toBe('Post1'); - - const read = await db.user.findUnique({ - where: { - id: '1', - }, - include: { posts: true }, - }); - expect(read.posts[0].title).toBe('Post1'); - - const rawRead = await prisma.user.findUnique({ where: { id: '1' }, include: { posts: true } }); - expect(rawRead.posts[0].title).not.toBe('Post1'); - }); - - it('Multi-field encryption test', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - x1 String @encrypted() - x2 String @encrypted() - }`, - { - enhancements: ['encryption'], - enhanceOptions: { - encryption: { encryptionKey }, - }, - } - ); - - const db = enhance(); - - const create = await db.user.create({ - data: { - id: '1', - x1: 'abc123', - x2: '123abc', - }, - }); - - const read = await db.user.findUnique({ - where: { - id: '1', - }, - }); - - expect(create).toMatchObject({ x1: 'abc123', x2: '123abc' }); - expect(read).toMatchObject({ x1: 'abc123', x2: '123abc' }); - }); - - it('Custom encryption test', async () => { - const { enhance, prisma } = await loadSchema(` - model User { - id String @id @default(cuid()) - encrypted_value String @encrypted() - }`); - - const db = enhance(undefined, { - kinds: ['encryption'], - encryption: { - encrypt: async (model: string, field: FieldInfo, data: string) => { - // Add _enc to the end of the input - return data + '_enc'; - }, - decrypt: async (model: string, field: FieldInfo, cipher: string) => { - // Remove _enc from the end of the input explicitly - if (cipher.endsWith('_enc')) { - return cipher.slice(0, -4); // Remove last 4 characters (_enc) - } - - return cipher; - }, - }, - }); - - const create = await db.user.create({ - data: { - id: '1', - encrypted_value: 'abc123', - }, - }); - - const read = await db.user.findUnique({ - where: { - id: '1', - }, - }); - - const rawRead = await prisma.user.findUnique({ - where: { - id: '1', - }, - }); - - expect(create.encrypted_value).toBe('abc123'); - expect(read.encrypted_value).toBe('abc123'); - expect(rawRead.encrypted_value).toBe('abc123_enc'); - }); - - it('Works with multiple decryption keys', async () => { - const { enhanceRaw: enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - secret String @encrypted() - }` - ); - - const key1 = crypto.getRandomValues(new Uint8Array(32)); - const key2 = crypto.getRandomValues(new Uint8Array(32)); - - const db1 = enhance(prisma, undefined, { - kinds: ['encryption'], - encryption: { encryptionKey: key1 }, - }); - const user1 = await db1.user.create({ data: { secret: 'user1' } }); - - const db2 = enhance(prisma, undefined, { - kinds: ['encryption'], - encryption: { encryptionKey: key2 }, - }); - const user2 = await db2.user.create({ data: { secret: 'user2' } }); - - const dbAll = enhance(prisma, undefined, { - kinds: ['encryption'], - encryption: { encryptionKey: crypto.getRandomValues(new Uint8Array(32)), decryptionKeys: [key1, key2] }, - }); - const allUsers = await dbAll.user.findMany(); - expect(allUsers).toEqual(expect.arrayContaining([user1, user2])); - - const dbWithEncryptionKeyExplicitlyProvided = enhance(prisma, undefined, { - kinds: ['encryption'], - encryption: { encryptionKey: key1, decryptionKeys: [key1, key2] }, - }); - await expect(dbWithEncryptionKeyExplicitlyProvided.user.findMany()).resolves.toEqual( - expect.arrayContaining([user1, user2]) - ); - - const dbWithDuplicatedKeys = enhance(prisma, undefined, { - kinds: ['encryption'], - encryption: { encryptionKey: key1, decryptionKeys: [key1, key1, key2, key2] }, - }); - await expect(dbWithDuplicatedKeys.user.findMany()).resolves.toEqual(expect.arrayContaining([user1, user2])); - - const dbWithInvalidKeys = enhance(prisma, undefined, { - kinds: ['encryption'], - encryption: { encryptionKey: key1, decryptionKeys: [key2, crypto.getRandomValues(new Uint8Array(32))] }, - }); - await expect(dbWithInvalidKeys.user.findMany()).resolves.toEqual(expect.arrayContaining([user1, user2])); - - const dbWithMissingKeys = enhance(prisma, undefined, { - kinds: ['encryption'], - encryption: { encryptionKey: key2 }, - }); - const found = await dbWithMissingKeys.user.findMany(); - expect(found).not.toContainEqual(user1); - expect(found).toContainEqual(user2); - - const dbWithAllWrongKeys = enhance(prisma, undefined, { - kinds: ['encryption'], - encryption: { encryptionKey: crypto.getRandomValues(new Uint8Array(32)) }, - }); - const found1 = await dbWithAllWrongKeys.user.findMany(); - expect(found1).not.toContainEqual(user1); - expect(found1).not.toContainEqual(user2); - }); - - it('Only supports string fields', async () => { - await expect( - loadModelWithError( - ` - model User { - id String @id @default(cuid()) - encrypted_value Bytes @encrypted() - }` - ) - ).resolves.toContain(`attribute \"@encrypted\" cannot be used on this type of field`); - }); - - it('Returns cipher text when decryption fails', async () => { - const { enhance, enhanceRaw, prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - encrypted_value String @encrypted() - - @@allow('all', true) - }`, - { enhancements: ['encryption'] } - ); - - const db = enhance(undefined, { - kinds: ['encryption'], - encryption: { encryptionKey }, - }); - - const create = await db.user.create({ - data: { - id: '1', - encrypted_value: 'abc123', - }, - }); - expect(create.encrypted_value).toBe('abc123'); - - const db1 = enhanceRaw(prisma, undefined, { - encryption: { encryptionKey: crypto.getRandomValues(new Uint8Array(32)) }, - }); - const read = await db1.user.findUnique({ where: { id: '1' } }); - expect(read.encrypted_value).toBeTruthy(); - expect(read.encrypted_value).not.toBe('abc123'); - }); - - it('Works with length validation', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - encrypted_value String @encrypted() @length(0, 6) - @@allow('all', true) - }`, - { - enhanceOptions: { encryption: { encryptionKey } }, - } - ); - - const db = enhance(); - - const create = await db.user.create({ - data: { - id: '1', - encrypted_value: 'abc123', - }, - }); - expect(create.encrypted_value).toBe('abc123'); - - await expect( - db.user.create({ - data: { id: '2', encrypted_value: 'abc1234' }, - }) - ).toBeRejectedByPolicy(); - }); - - it('Complains when encrypted fields are used in model-level policy rules', async () => { - await expect( - loadModelWithError(` - model User { - id String @id @default(cuid()) - encrypted_value String @encrypted() - @@allow('all', encrypted_value != 'abc123') - } - `) - ).resolves.toContain(`Encrypted fields cannot be used in policy rules`); - }); - - it('Complains when encrypted fields are used in field-level policy rules', async () => { - await expect( - loadModelWithError(` - model User { - id String @id @default(cuid()) - encrypted_value String @encrypted() - value Int @allow('all', encrypted_value != 'abc123') - } - `) - ).resolves.toContain(`Encrypted fields cannot be used in policy rules`); - }); -}); diff --git a/tests/integration/tests/enhancements/with-omit/with-omit.test.ts b/tests/integration/tests/enhancements/with-omit/with-omit.test.ts deleted file mode 100644 index 73fdaf806..000000000 --- a/tests/integration/tests/enhancements/with-omit/with-omit.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('Omit test', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(async () => { - process.chdir(origDir); - }); - - const model = ` - model User { - id String @id @default(cuid()) - password String @omit - profile Profile? - - @@allow('all', true) - } - - model Profile { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String @unique - image String @omit - - @@allow('all', true) - } - `; - - it('omit tests', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - const r = await db.user.create({ - include: { profile: true }, - data: { - id: '1', - password: 'abc123', - profile: { - create: { - image: 'an image', - }, - }, - }, - }); - expect(r.password).toBeUndefined(); - expect(r.profile.image).toBeUndefined(); - - const r1 = await db.user.findUnique({ - where: { id: '1' }, - include: { profile: true }, - }); - expect(r1.password).toBeUndefined(); - expect(r1.profile.image).toBeUndefined(); - - await db.user.create({ - include: { profile: true }, - data: { - id: '2', - password: 'abc234', - profile: { - create: { - image: 'another image', - }, - }, - }, - }); - - const r2 = await db.user.findMany({ include: { profile: true } }); - r2.forEach((e: any) => { - expect(e.password).toBeUndefined(); - expect(e.profile.image).toBeUndefined(); - }); - }); - - it('customization', async () => { - const { prisma, enhance } = await loadSchema(model, { - output: './zen', - enhancements: ['omit'], - }); - - const db = enhance(prisma); - const r = await db.user.create({ - include: { profile: true }, - data: { - id: '1', - password: 'abc123', - profile: { create: { image: 'an image' } }, - }, - }); - expect(r.password).toBeUndefined(); - expect(r.profile.image).toBeUndefined(); - - const db1 = enhance(prisma, { modelMeta: require(path.resolve('./zen/model-meta')).default }); - const r1 = await db1.user.create({ - include: { profile: true }, - data: { - id: '2', - password: 'abc123', - profile: { create: { image: 'an image' } }, - }, - }); - expect(r1.password).toBeUndefined(); - expect(r1.profile.image).toBeUndefined(); - }); - - it('to-many', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - posts Post[] - - @@allow('all', true) - } - - model Post { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String - images Image[] - - @@allow('all', true) - } - - model Image { - id String @id @default(cuid()) - post Post @relation(fields: [postId], references: [id]) - postId String - url String @omit - - @@allow('all', true) - } - `, - { enhancements: ['omit'] } - ); - - const db = enhance(); - const r = await db.user.create({ - include: { posts: { include: { images: true } } }, - data: { - posts: { - create: [ - { images: { create: { url: 'img1' } } }, - { images: { create: [{ url: 'img2' }, { url: 'img3' }] } }, - ], - }, - }, - }); - - expect(r.posts[0].images[0].url).toBeUndefined(); - expect(r.posts[1].images[0].url).toBeUndefined(); - expect(r.posts[1].images[1].url).toBeUndefined(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-password/with-password.test.ts b/tests/integration/tests/enhancements/with-password/with-password.test.ts deleted file mode 100644 index 5184ea539..000000000 --- a/tests/integration/tests/enhancements/with-password/with-password.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import { compareSync } from 'bcryptjs'; -import path from 'path'; - -describe('Password test', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(async () => { - process.chdir(origDir); - }); - - it('password tests', async () => { - const { enhance, prisma } = await loadSchema(` - model User { - id String @id @default(cuid()) - password String @password(saltLength: 16) - - @@allow('all', true) - }`); - - const db = enhance(); - const r = await db.user.create({ - data: { - id: '1', - password: 'abc123', - }, - }); - expect(compareSync('abc123', r.password)).toBeTruthy(); - - const r1 = await db.user.update({ - where: { id: '1' }, - data: { - password: 'abc456', - }, - }); - expect(compareSync('abc456', r1.password)).toBeTruthy(); - - await db.user.createMany({ - data: [ - { id: '2', password: 'user2' }, - { id: '3', password: 'user3' }, - ], - }); - await expect(prisma.user.findUnique({ where: { id: '2' } })).resolves.not.toMatchObject({ password: 'user2' }); - const r2 = await db.user.findUnique({ where: { id: '2' } }); - expect(compareSync('user2', r2.password)).toBeTruthy(); - - const [u4] = await db.user.createManyAndReturn({ - data: [ - { id: '4', password: 'user4' }, - { id: '5', password: 'user5' }, - ], - }); - expect(compareSync('user4', u4.password)).toBeTruthy(); - await expect(prisma.user.findUnique({ where: { id: '4' } })).resolves.not.toMatchObject({ password: 'user4' }); - const r4 = await db.user.findUnique({ where: { id: '4' } }); - expect(compareSync('user4', r4.password)).toBeTruthy(); - }); - - it('length tests', async () => { - const { enhance } = await loadSchema(` - model User { - id String @id @default(cuid()) - password String @password(saltLength: 16) @length(1, 8) @startsWith('abc') - - @@allow('all', true) - }`); - - const db = enhance(); - let r = await db.user.create({ - data: { - id: '1', - password: 'abc123', - }, - }); - expect(compareSync('abc123', r.password)).toBeTruthy(); - - r = await db.user.update({ - where: { id: '1' }, - data: { - password: 'abc456', - }, - }); - expect(compareSync('abc456', r.password)).toBeTruthy(); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - password: 'abc456789', - }, - }) - ).toBeRejectedByPolicy(['8 character']); - - await expect( - db.user.create({ - data: { - id: '2', - password: 'abc456789', - }, - }) - ).toBeRejectedByPolicy(['8 character']); - - await expect( - db.user.create({ - data: { - id: '2', - password: '123456', - }, - }) - ).toBeRejectedByPolicy(['must start with "abc" at "password"']); - }); - - it('prevents query enumeration if password is not readable', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - email String @unique - password String @password @omit @deny('read', true) - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - const user = await db.user.create({ - data: { - email: 'alice@abc.com', - password: '123456', - }, - }); - expect(user.password).toBeUndefined(); - - const u = await prisma.user.findFirstOrThrow(); - - await expect(db.user.findFirst({ where: { password: u.password } })).toResolveNull(); - await expect( - db.user.findFirst({ - where: { password: { startsWith: u?.password.substring(0, 3) } }, - }) - ).toResolveNull(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/abstract.test.ts b/tests/integration/tests/enhancements/with-policy/abstract.test.ts deleted file mode 100644 index 0987df37d..000000000 --- a/tests/integration/tests/enhancements/with-policy/abstract.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Abstract models', () => { - it('connect test1', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? @relation(fields: [profileId], references: [id]) - profileId Int? @unique - - @@allow('create,read', true) - @@allow('update', auth().id == 1) - } - - abstract model BaseProfile { - id Int @id @default(autoincrement()) - user User? - - @@allow('all', true) - } - - model Profile extends BaseProfile { - name String - } - ` - ); - - const db = enhance({ id: 2 }); - const user = await db.user.create({ data: { id: 1 } }); - const profile = await db.profile.create({ data: { id: 1, name: 'John' } }); - await expect( - db.profile.update({ where: { id: 1 }, data: { user: { connect: { id: user.id } } } }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ where: { id: 1 }, data: { profile: { connect: { id: profile.id } } } }) - ).toBeRejectedByPolicy(); - - const db1 = enhance({ id: 1 }); - await expect( - db1.profile.update({ where: { id: 1 }, data: { user: { connect: { id: user.id } } } }) - ).toResolveTruthy(); - await expect( - db1.user.update({ where: { id: 1 }, data: { profile: { connect: { id: profile.id } } } }) - ).toResolveTruthy(); - }); - - it('connect test2', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - - @@allow('all', true) - } - - abstract model BaseProfile { - id Int @id @default(autoincrement()) - user User? @relation(fields: [userId], references: [id]) - userId Int? @unique - - @@allow('create,read', true) - @@allow('update', auth().id == 1) - } - - model Profile extends BaseProfile { - name String - } - ` - ); - - const db = enhance({ id: 2 }); - const user = await db.user.create({ data: { id: 1 } }); - const profile = await db.profile.create({ data: { id: 1, name: 'John' } }); - await expect( - db.profile.update({ where: { id: 1 }, data: { user: { connect: { id: user.id } } } }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ where: { id: 1 }, data: { profile: { connect: { id: profile.id } } } }) - ).toBeRejectedByPolicy(); - - const db1 = enhance({ id: 1 }); - await expect( - db1.profile.update({ where: { id: 1 }, data: { user: { connect: { id: user.id } } } }) - ).toResolveTruthy(); - await expect( - db1.user.update({ where: { id: 1 }, data: { profile: { connect: { id: profile.id } } } }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/auth.test.ts b/tests/integration/tests/enhancements/with-policy/auth.test.ts deleted file mode 100644 index 081c14238..000000000 --- a/tests/integration/tests/enhancements/with-policy/auth.test.ts +++ /dev/null @@ -1,994 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('auth() runtime test', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('undefined user with string id simple', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - } - - model Post { - id String @id @default(uuid()) - title String - - @@allow('read', true) - @@allow('create', auth() != null) - } - ` - ); - - const db = enhance(); - await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - - const authDb = enhance({ id: 'user1' }); - await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); - }); - - it('undefined user with string id more', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - } - - model Post { - id String @id @default(uuid()) - title String - - @@allow('read', true) - @@allow('create', auth().id != null) - } - ` - ); - - const db = enhance(); - await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - - const authDb = enhance({ id: 'user1' }); - await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); - }); - - it('undefined user with int id', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - } - - model Post { - id String @id @default(uuid()) - title String - - @@allow('read', true) - @@allow('create', auth() != null) - } - ` - ); - - const db = enhance(); - await expect(db.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - - const authDb = enhance({ id: 'user1' }); - await expect(authDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); - }); - - it('undefined user compared with field', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - posts Post[] - - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId String - - @@allow('create,read', true) - @@allow('update', auth() == author) - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 'user1' } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: '1', title: 'abc', authorId: 'user1' } })).toResolveTruthy(); - - const authDb = enhance(); - await expect(authDb.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - - expect(() => enhance({ id: null })).toThrow(/Invalid user context/); - - const authDb2 = enhance({ id: 'user1' }); - await expect(authDb2.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); - }); - - it('undefined user compared with field more', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - posts Post[] - - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId String - - @@allow('create,read', true) - @@allow('update', auth().id == author.id) - } - ` - ); - - const db = enhance(); - 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' } })).toBeRejectedByPolicy(); - - const authDb2 = enhance({ id: 'user1' }); - await expect(authDb2.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); - }); - - it('undefined user non-id field', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - posts Post[] - role String - - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId String - - @@allow('create,read', true) - @@allow('update', auth().role == 'ADMIN') - } - ` - ); - - const db = enhance(); - 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' } })).toBeRejectedByPolicy(); - - const authDb = enhance({ id: 'user1', role: 'USER' }); - await expect(authDb.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toBeRejectedByPolicy(); - - const authDb1 = enhance({ id: 'user2', role: 'ADMIN' }); - await expect(authDb1.post.update({ where: { id: '1' }, data: { title: 'bcd' } })).toResolveTruthy(); - }); - - it('non User auth model', async () => { - const { enhance } = await loadSchema( - ` - model Foo { - id String @id @default(uuid()) - role String - - @@auth() - } - - model Post { - id String @id @default(uuid()) - title String - - @@allow('read', true) - @@allow('create', auth().role == 'ADMIN') - } - ` - ); - - const userDb = enhance({ id: 'user1', role: 'USER' }); - await expect(userDb.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - - const adminDb = enhance({ id: 'user1', role: 'ADMIN' }); - await expect(adminDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); - }); - - it('User model ignored', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - role String - - @@ignore - } - - model Post { - id String @id @default(uuid()) - title String - - @@allow('read', true) - @@allow('create', auth().role == 'ADMIN') - } - ` - ); - - const userDb = enhance({ id: 'user1', role: 'USER' }); - await expect(userDb.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - - const adminDb = enhance({ id: 'user1', role: 'ADMIN' }); - await expect(adminDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); - }); - - it('Auth model ignored', async () => { - const { enhance } = await loadSchema( - ` - model Foo { - id String @id @default(uuid()) - role String - - @@auth() - @@ignore - } - - model Post { - id String @id @default(uuid()) - title String - - @@allow('read', true) - @@allow('create', auth().role == 'ADMIN') - } - ` - ); - - const userDb = enhance({ id: 'user1', role: 'USER' }); - await expect(userDb.post.create({ data: { title: 'abc' } })).toBeRejectedByPolicy(); - - const adminDb = enhance({ id: 'user1', role: 'ADMIN' }); - await expect(adminDb.post.create({ data: { title: 'abc' } })).toResolveTruthy(); - }); - - it('collection predicate', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - posts Post[] - - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String - comments Comment[] - - @@allow('read', true) - @@allow('create', auth().posts?[published && comments![published]]) - } - - model Comment { - id String @id @default(uuid()) - published Boolean @default(false) - post Post @relation(fields: [postId], references: [id]) - postId String - - @@allow('all', true) - } - ` - ); - - const user = await prisma.user.create({ data: {} }); - - const createPayload = { - data: { title: 'Post 1', author: { connect: { id: user.id } } }, - }; - - // no post - await expect(enhance({ id: '1' }).post.create(createPayload)).toBeRejectedByPolicy(); - - // post not published - await expect( - enhance({ id: '1', posts: [{ id: '1', published: false }] }).post.create(createPayload) - ).toBeRejectedByPolicy(); - - // no comments - await expect( - enhance({ id: '1', posts: [{ id: '1', published: true }] }).post.create(createPayload) - ).toBeRejectedByPolicy(); - - // not all comments published - await expect( - enhance({ - id: '1', - posts: [ - { - id: '1', - published: true, - comments: [ - { id: '1', published: true }, - { id: '2', published: false }, - ], - }, - ], - }).post.create(createPayload) - ).toBeRejectedByPolicy(); - - // comments published but parent post is not - await expect( - enhance({ - id: '1', - posts: [ - { id: '1', published: false, comments: [{ id: '1', published: true }] }, - { id: '2', published: true }, - ], - }).post.create(createPayload) - ).toBeRejectedByPolicy(); - - await expect( - enhance({ - id: '1', - posts: [ - { id: '1', published: true, comments: [{ id: '1', published: true }] }, - { id: '2', published: false }, - ], - }).post.create(createPayload) - ).toResolveTruthy(); - - // no comments ("every" evaluates to tru in this case) - await expect( - enhance({ id: '1', posts: [{ id: '1', published: true, comments: [] }] }).post.create(createPayload) - ).toResolveTruthy(); - }); - - it('Default auth() on literal fields', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - name String - score Int - - } - - model Post { - id String @id @default(uuid()) - title String - score Int? @default(auth().score) - authorName String? @default(auth().name) - - @@allow('all', true) - } - ` - ); - - const userDb = enhance({ id: '1', name: 'user1', score: 10 }); - 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.createMany({ data: [{ title: 'def' }] })).resolves.toMatchObject({ count: 1 }); - const r = await userDb.post.createManyAndReturn({ data: [{ title: 'xxx' }, { title: 'yyy' }] }); - expect(r[0]).toMatchObject({ title: 'xxx', score: 10 }); - expect(r[1]).toMatchObject({ title: 'yyy', score: 10 }); - }); - - it('Default auth() data should not override passed args', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - name String - - } - - model Post { - id String @id @default(uuid()) - authorName String? @default(auth().name) - - @@allow('all', true) - } - ` - ); - - const userContextName = 'user1'; - const overrideName = 'no-default-auth-name'; - const userDb = enhance({ 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.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 }] }); - expect(r[0]).toMatchObject({ authorName: overrideName }); - }); - - it('Default auth() with foreign key', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id String @id - email String @unique - posts Post[] - - @@allow('all', true) - - } - - model Post { - id String @id @default(uuid()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 'userId-1', email: 'user1@abc.com' } }); - await prisma.user.create({ data: { id: 'userId-2', email: 'user2@abc.com' } }); - - const db = enhance({ id: 'userId-1' }); - - // default auth effective - await expect(db.post.create({ data: { title: 'post1' } })).resolves.toMatchObject({ authorId: 'userId-1' }); - - // default auth ineffective due to explicit connect - await expect( - db.post.create({ data: { title: 'post2', author: { connect: { email: 'user1@abc.com' } } } }) - ).resolves.toMatchObject({ authorId: 'userId-1' }); - - // default auth ineffective due to explicit connect - await expect( - db.post.create({ data: { title: 'post3', author: { connect: { email: 'user2@abc.com' } } } }) - ).resolves.toMatchObject({ authorId: 'userId-2' }); - - // upsert - await expect( - db.post.upsert({ - where: { id: 'post4' }, - create: { id: 'post4', title: 'post4' }, - update: { title: 'post4' }, - }) - ).resolves.toMatchObject({ authorId: 'userId-1' }); - - // default auth effective for createMany - 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' }); - - // default auth effective for createManyAndReturn - const r1 = await db.post.createManyAndReturn({ data: { title: 'post6' } }); - expect(r1[0]).toMatchObject({ authorId: 'userId-1' }); - }); - - it('Default auth() with nested user context value', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - profile Profile? - posts Post[] - - @@allow('all', true) - } - - model Profile { - id String @id @default(uuid()) - image Image? - user User @relation(fields: [userId], references: [id]) - userId String @unique - } - - model Image { - id String @id @default(uuid()) - url String - profile Profile @relation(fields: [profileId], references: [id]) - profileId String @unique - } - - model Post { - id String @id @default(uuid()) - title String - defaultImageUrl String @default(auth().profile.image.url) - author User @relation(fields: [authorId], references: [id]) - authorId String - - @@allow('all', true) - } - ` - ); - const url = 'https://zenstack.dev'; - const db = enhance({ id: 'userId-1', profile: { image: { url } } }); - - // top-level create - 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({ - data: { - id: 'userId-2', - posts: { - create: [{ title: 'p1' }, { title: 'p2' }], - }, - }, - include: { posts: true }, - }); - expect(result.posts).toEqual( - expect.arrayContaining([ - expect.objectContaining({ title: 'p1', defaultImageUrl: url }), - expect.objectContaining({ title: 'p2', defaultImageUrl: url }), - ]) - ); - }); - - it('Default auth() without user context', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - posts Post[] - - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 'userId-1' } })).toResolveTruthy(); - await expect(db.post.create({ data: { title: 'title' } })).rejects.toThrow( - 'Evaluating default value of field `authorId` requires a user context' - ); - await expect(db.post.findMany({})).toResolveTruthy(); - }); - - it('Default auth() field optionality', async () => { - await loadSchema( - ` - model User { - id String @id - posts Post[] - } - - model Post { - id String @id @default(uuid()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - - const prisma = new PrismaClient(); - const db = enhance(prisma, { user: { id: 'user1' } }); - - // "author" and "authorId" are optional - db.post.create({ data: { title: 'abc' } }); -`, - }, - ], - } - ); - }); - - it('Default auth() safe unsafe mix', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id - posts Post[] - - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) - - stats Stats @relation(fields: [statsId], references: [id]) - statsId String @unique - - @@allow('all', true) - } - - model Stats { - id String @id @default(uuid()) - viewCount Int @default(0) - post Post? - - @@allow('all', true) - - } - ` - ); - - const db = enhance({ id: 'userId-1' }); - await db.user.create({ data: { id: 'userId-1' } }); - - // unsafe - await db.stats.create({ data: { id: 'stats-1', viewCount: 10 } }); - 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, - }); - - await db.stats.create({ data: { id: 'stats-3', viewCount: 10 } }); - const r = await db.post.createManyAndReturn({ data: [{ title: 'title3', statsId: 'stats-3' }] }); - expect(r[0]).toMatchObject({ statsId: 'stats-3' }); - - // safe - await db.stats.create({ data: { id: 'stats-4', viewCount: 10 } }); - await expect( - db.post.create({ data: { title: 'title4', stats: { connect: { id: 'stats-4' } } } }) - ).toResolveTruthy(); - }); -}); - -describe('auth() compile-time test', () => { - it('default enhanced typing', async () => { - await loadSchema( - ` - model User { - id1 Int - id2 Int - age Int - - @@id([id1, id2]) - @@allow('all', true) - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from ".zenstack/enhance"; - import { PrismaClient } from '@prisma/client'; - enhance(new PrismaClient(), { user: { id1: 1, id2: 2 } }); - `, - }, - ], - } - ); - }); - - it('custom auth model', async () => { - await loadSchema( - ` - model Foo { - id Int @id - age Int - - @@auth - @@allow('all', true) - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from ".zenstack/enhance"; - import { PrismaClient } from '@prisma/client'; - enhance(new PrismaClient(), { user: { id: 1 } }); - `, - }, - ], - } - ); - }); - - it('auth() selection', async () => { - await loadSchema( - ` - model User { - id Int @id - age Int - email String - - @@allow('all', auth().age > 0) - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from ".zenstack/enhance"; - import { PrismaClient } from '@prisma/client'; - enhance(new PrismaClient(), { user: { id: 1, age: 10 } }); - `, - }, - ], - } - ); - }); - - it('auth() to-one relation selection', async () => { - await loadSchema( - ` - model User { - id Int @id - email String - profile Profile? - - @@allow('all', auth().profile.age > 0 && auth().profile.job.level > 0) - } - - model Profile { - id Int @id - job Job? - age Int - user User @relation(fields: [userId], references: [id]) - userId Int @unique - } - - model Job { - id Int @id - level Int - profile Profile @relation(fields: [profileId], references: [id]) - profileId Int @unique - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from ".zenstack/enhance"; - import { PrismaClient } from '@prisma/client'; - enhance(new PrismaClient(), { user: { id: 1, profile: { age: 1, job: { level: 10 } } } }); - `, - }, - ], - } - ); - }); - - it('auth() to-many relation selection', async () => { - await loadSchema( - ` - model User { - id Int @id - email String - posts Post[] - - @@allow('all', auth().posts?[viewCount > 0] && auth().posts?[comments?[level > 0]]) - } - - model Post { - id Int @id - viewCount Int - comments Comment[] - user User @relation(fields: [userId], references: [id]) - userId Int - } - - model Comment { - id Int @id - level Int - post Post @relation(fields: [postId], references: [id]) - postId Int - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from ".zenstack/enhance"; - import { PrismaClient } from '@prisma/client'; - enhance(new PrismaClient(), { user: { id: 1, posts: [ { viewCount: 1, comments: [ { level: 1 } ] } ] } }); - `, - }, - ], - } - ); - }); - - it('optional field stays optional', async () => { - await loadSchema( - ` - model User { - id Int @id - age Int? - - @@allow('all', auth().age > 0) - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from ".zenstack/enhance"; - import { PrismaClient } from '@prisma/client'; - enhance(new PrismaClient(), { user: { id: 1 } }); - `, - }, - ], - } - ); - }); - - it('"User" type as auth legacy generator', async () => { - const { enhance } = await loadSchema( - ` - type Profile { - age Int - } - - type Role { - name String - permissions String[] - } - - type User { - myId Int @id - banned Boolean - profile Profile - roles Role[] - } - - model Foo { - id Int @id @default(autoincrement()) - @@allow('read', true) - @@allow('create', auth().myId == 1 && !auth().banned) - @@allow('delete', auth().roles?['DELETE' in permissions]) - @@deny('all', auth().profile.age < 18) - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from ".zenstack/enhance"; - import { PrismaClient } from '@prisma/client'; - enhance(new PrismaClient(), { user: { myId: 1, profile: { age: 20 } } }); - `, - }, - ], - } - ); - - await expect(enhance().foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ myId: 1, banned: true }).foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ myId: 1, profile: { age: 16 } }).foo.create({ data: {} })).toBeRejectedByPolicy(); - const r = await enhance({ myId: 1, profile: { age: 20 } }).foo.create({ data: {} }); - await expect( - enhance({ myId: 1, profile: { age: 20 } }).foo.delete({ where: { id: r.id } }) - ).toBeRejectedByPolicy(); - await expect( - enhance({ myId: 1, profile: { age: 20 }, roles: [{ name: 'ADMIN', permissions: ['DELETE'] }] }).foo.delete({ - where: { id: r.id }, - }) - ).toResolveTruthy(); - }); - - it('"User" type as auth new generator', async () => { - const { enhance } = await loadSchema( - ` - datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - - generator js { - provider = "prisma-client" - output = "./generated/client" - moduleFormat = "cjs" - } - - type Profile { - age Int - } - - type Role { - name String - permissions String[] - } - - type User { - myId Int @id - banned Boolean - profile Profile - roles Role[] - } - - model Foo { - id Int @id @default(autoincrement()) - @@allow('read', true) - @@allow('create', auth().myId == 1 && !auth().banned) - @@allow('delete', auth().roles?['DELETE' in permissions]) - @@deny('all', auth().profile.age < 18) - } - `, - { - addPrelude: false, - output: './zenstack', - preserveTsFiles: true, - prismaLoadPath: './prisma/generated/client/client', - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from "./zenstack/enhance"; - import { PrismaClient } from '@prisma/client'; - enhance(new PrismaClient(), { user: { myId: 1, profile: { age: 20 } } }); - `, - }, - ], - } - ); - - await expect(enhance().foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ myId: 1, banned: true }).foo.create({ data: {} })).toBeRejectedByPolicy(); - await expect(enhance({ myId: 1, profile: { age: 16 } }).foo.create({ data: {} })).toBeRejectedByPolicy(); - const r = await enhance({ myId: 1, profile: { age: 20 } }).foo.create({ data: {} }); - await expect( - enhance({ myId: 1, profile: { age: 20 } }).foo.delete({ where: { id: r.id } }) - ).toBeRejectedByPolicy(); - await expect( - enhance({ myId: 1, profile: { age: 20 }, roles: [{ name: 'ADMIN', permissions: ['DELETE'] }] }).foo.delete({ - where: { id: r.id }, - }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/cal-com-sample.test.ts b/tests/integration/tests/enhancements/with-policy/cal-com-sample.test.ts deleted file mode 100644 index 4ebc7f448..000000000 --- a/tests/integration/tests/enhancements/with-policy/cal-com-sample.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { loadSchemaFromFile } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('Cal.com Sample Integration Tests', () => { - it('model loading', async () => { - await loadSchemaFromFile(path.join(__dirname, '../../schema/cal-com.zmodel'), { - addPrelude: false, - pushDb: false, - }); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/checker.test.ts b/tests/integration/tests/enhancements/with-policy/checker.test.ts deleted file mode 100644 index a109c3ef6..000000000 --- a/tests/integration/tests/enhancements/with-policy/checker.test.ts +++ /dev/null @@ -1,763 +0,0 @@ -import { SchemaLoadOptions, createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('Permission checker', () => { - const PRELUDE = ` - datasource db { - provider = 'sqlite' - url = 'file:./dev.db' - } - - generator js { - provider = 'prisma-client-js' - } - - plugin enhancer { - provider = '@core/enhancer' - generatePermissionChecker = true - } - `; - - const load = (schema: string, options?: SchemaLoadOptions) => - loadSchema(schema, { - ...options, - generatePermissionChecker: true, - }); - - it('checker generation not enabled', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('all', true) - } - ` - ); - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).rejects.toThrow('Generated permission checkers not found'); - }); - - it('empty rules', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - } - ` - ); - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveFalsy(); - }); - - it('unconditional allow', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('all', true) - } - ` - ); - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 0 } })).toResolveTruthy(); - }); - - it('multiple allow rules', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('all', value == 1) - @@allow('all', value == 2) - } - ` - ); - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 0 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 2 } })).toResolveTruthy(); - }); - - it('deny rule', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('all', value > 0) - @@deny('all', value == 1) - } - ` - ); - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 0 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { value: 2 } })).toResolveTruthy(); - }); - - it('int field condition', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('read', value == 1) - @@allow('create', value != 1) - @@allow('update', value > 1) - @@allow('delete', value <= 1) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 0 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy(); - - await expect(db.model.check({ operation: 'create' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'create', where: { value: 0 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'create', where: { value: 1 } })).toResolveFalsy(); - - await expect(db.model.check({ operation: 'update' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'update', where: { value: 1 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'update', where: { value: 2 } })).toResolveTruthy(); - - await expect(db.model.check({ operation: 'delete' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'delete', where: { value: 0 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'delete', where: { value: 1 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'delete', where: { value: 2 } })).toResolveFalsy(); - }); - - it('boolean field toplevel condition', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Boolean - @@allow('read', value) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: false } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { value: true } })).toResolveTruthy(); - }); - - it('boolean field condition', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Boolean - @@allow('read', value == true) - @@allow('create', value == false) - @@allow('update', value != true) - @@allow('delete', value != false) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: false } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { value: true } })).toResolveTruthy(); - - await expect(db.model.check({ operation: 'create' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'create', where: { value: true } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'create', where: { value: false } })).toResolveTruthy(); - - await expect(db.model.check({ operation: 'update' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'update', where: { value: true } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'update', where: { value: false } })).toResolveTruthy(); - - await expect(db.model.check({ operation: 'delete' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'delete', where: { value: false } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'delete', where: { value: true } })).toResolveTruthy(); - }); - - it('string field condition', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value String - @@allow('read', value == 'admin') - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 'user' } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { value: 'admin' } })).toResolveTruthy(); - }); - - it('enum', async () => { - const dbUrl = await createPostgresDb('permission-checker-enum'); - let prisma: any; - try { - const r = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = '${dbUrl}' - } - - generator js { - provider = 'prisma-client-js' - } - - plugin enhancer { - provider = '@core/enhancer' - generatePermissionChecker = true - } - - enum Role { - USER - ADMIN - } - model User { - id Int @id @default(autoincrement()) - role Role - } - model Model { - id Int @id @default(autoincrement()) - @@allow('read', auth().role == ADMIN) - } - `, - { addPrelude: false, generatePermissionChecker: true } - ); - - prisma = r.prisma; - const enhance = r.enhance; - - await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1, role: 'USER' }).model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1, role: 'ADMIN' }).model.check({ operation: 'read' })).toResolveTruthy(); - } finally { - await prisma.$disconnect(); - await dropPostgresDb('permission-checker-enum'); - } - }); - - it('function noop', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value String - @@allow('read', startsWith(value, 'admin')) - @@allow('update', !startsWith(value, 'admin')) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 'user' } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 'admin' } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'update' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'update', where: { value: 'user' } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'update', where: { value: 'admin' } })).toResolveTruthy(); - }); - - it('relation noop', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value String - foo Foo? - - @@allow('read', foo.x > 0) - } - - model Foo { - id Int @id @default(autoincrement()) - x Int - modelId Int @unique - model Model @relation(fields: [modelId], references: [id]) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { foo: { x: 0 } } })).rejects.toThrow( - 'Providing filter for field "foo"' - ); - }); - - it('collection predicate noop', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value String - foo Foo[] - - @@allow('read', foo?[x > 0]) - } - - model Foo { - id Int @id @default(autoincrement()) - x Int - modelId Int - model Model @relation(fields: [modelId], references: [id]) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { foo: [{ x: 0 }] } })).rejects.toThrow( - 'Providing filter for field "foo"' - ); - }); - - it('field complex condition', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int - @@allow('read', x > 0 && x > y) - @@allow('create', x > 1 || x > y) - @@allow('update', !(x >= y)) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { x: 0 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { x: 1 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { x: 1, y: 0 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { x: 1, y: 1 } })).toResolveFalsy(); - - await expect(db.model.check({ operation: 'create' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'create', where: { x: 0 } })).toResolveFalsy(); // numbers are non-negative - await expect(db.model.check({ operation: 'create', where: { x: 1 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'create', where: { x: 1, y: 0 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'create', where: { x: 1, y: 1 } })).toResolveFalsy(); - - await expect(db.model.check({ operation: 'update' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'update', where: { x: 0 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'update', where: { y: 0 } })).toResolveFalsy(); // numbers are non-negative - await expect(db.model.check({ operation: 'update', where: { x: 1, y: 1 } })).toResolveFalsy(); - }); - - it('field condition unsatisfiable', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int - @@allow('read', x > 0 && x < y && y <= 1) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { x: 0 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { x: 1 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { x: 1, y: 2 } })).toResolveFalsy(); - await expect(db.model.check({ operation: 'read', where: { x: 1, y: 1 } })).toResolveFalsy(); - }); - - it('simple auth condition', async () => { - const { enhance } = await load( - ` - model User { - id Int @id @default(autoincrement()) - level Int - admin Boolean - } - - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('read', auth().level > 0) - @@allow('create', auth().admin) - @@allow('update', !auth().admin) - } - ` - ); - - await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1, level: 0 }).model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1, level: 1 }).model.check({ operation: 'read' })).toResolveTruthy(); - - await expect(enhance().model.check({ operation: 'create' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create' })).toResolveFalsy(); - await expect(enhance({ id: 1, admin: false }).model.check({ operation: 'create' })).toResolveFalsy(); - await expect(enhance({ id: 1, admin: true }).model.check({ operation: 'create' })).toResolveTruthy(); - - await expect(enhance().model.check({ operation: 'update' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update' })).toResolveTruthy(); - await expect(enhance({ id: 1, admin: true }).model.check({ operation: 'update' })).toResolveFalsy(); - await expect(enhance({ id: 1, admin: false }).model.check({ operation: 'update' })).toResolveTruthy(); - }); - - it('auth compared with relation field', async () => { - const { enhance } = await load( - ` - model User { - id Int @id @default(autoincrement()) - models Model[] - } - - model Model { - id Int @id @default(autoincrement()) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - @@allow('read', auth().id == ownerId) - @@allow('create', auth().id != ownerId) - @@allow('update', auth() == owner) - @@allow('delete', auth() != owner) - } - `, - { preserveTsFiles: true } - ); - - await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read', where: { ownerId: 1 } })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read', where: { ownerId: 2 } })).toResolveFalsy(); - - await expect(enhance().model.check({ operation: 'create' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create', where: { ownerId: 1 } })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create', where: { ownerId: 2 } })).toResolveTruthy(); - - await expect(enhance().model.check({ operation: 'update' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update', where: { ownerId: 1 } })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update', where: { ownerId: 2 } })).toResolveFalsy(); - - await expect(enhance().model.check({ operation: 'delete' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'delete' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'delete', where: { ownerId: 1 } })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'delete', where: { ownerId: 2 } })).toResolveTruthy(); - }); - - it('auth null check', async () => { - const { enhance } = await load( - ` - model User { - id Int @id @default(autoincrement()) - level Int - } - - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('read', auth() != null) - @@allow('create', auth() == null) - @@allow('update', auth().level > 0) - } - ` - ); - - await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read' })).toResolveTruthy(); - - await expect(enhance().model.check({ operation: 'create' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create' })).toResolveFalsy(); - - await expect(enhance().model.check({ operation: 'update' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update' })).toResolveFalsy(); - await expect(enhance({ id: 1, level: 0 }).model.check({ operation: 'update' })).toResolveFalsy(); - await expect(enhance({ id: 1, level: 1 }).model.check({ operation: 'update' })).toResolveTruthy(); - }); - - it('auth with relation access', async () => { - const { enhance } = await load( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - } - - model Profile { - id Int @id @default(autoincrement()) - level Int - user User @relation(fields: [userId], references: [id]) - userId Int @unique - } - - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('read', auth().profile.level > 0) - } - ` - ); - - await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1, profile: { level: 0 } }).model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1, profile: { level: 1 } }).model.check({ operation: 'read' })).toResolveTruthy(); - }); - - it('nullable field', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int? - @@allow('read', value != null) - @@allow('create', value == null) - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'create' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'create', where: { value: 1 } })).toResolveTruthy(); - }); - - it('compilation', async () => { - await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('read', value == 1) - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - - const prisma = new PrismaClient(); - const db = enhance(prisma); - db.model.check({ operation: 'read' }); - db.model.check({ operation: 'read', where: { value: 1 }}); - `, - }, - ], - } - ); - }); - - it('invalid filter', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - foo Foo? - d DateTime - - @@allow('read', value == 1) - } - - model Foo { - id Int @id @default(autoincrement()) - x Int - model Model @relation(fields: [modelId], references: [id]) - modelId Int @unique - } - ` - ); - - const db = enhance(); - await expect(db.model.check({ operation: 'read', where: { foo: { x: 1 } } })).rejects.toThrow( - `Providing filter for field "foo" is not supported. Only scalar fields are allowed.` - ); - await expect(db.model.check({ operation: 'read', where: { d: new Date() } })).rejects.toThrow( - `Providing filter for field "d" is not supported. Only number, string, and boolean fields are allowed.` - ); - await expect(db.model.check({ operation: 'read', where: { value: null } })).rejects.toThrow( - `Using "null" as filter value is not supported yet` - ); - await expect(db.model.check({ operation: 'read', where: { value: {} } })).rejects.toThrow( - 'Invalid value type for field "value". Only number, string or boolean is allowed.' - ); - await expect(db.model.check({ operation: 'read', where: { value: 'abc' } })).rejects.toThrow( - 'Invalid value type for field "value". Expected "number"' - ); - await expect(db.model.check({ operation: 'read', where: { value: -1 } })).rejects.toThrow( - 'Invalid value for field "value". Only non-negative integers are allowed.' - ); - }); - - it('float field ignored', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Float - @@allow('read', value == 1.1) - } - ` - ); - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy(); - }); - - it('float value ignored', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('read', value > 1.1) - } - ` - ); - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 2 } })).toResolveTruthy(); - }); - - it('negative value ignored', async () => { - const { enhance } = await load( - ` - model Model { - id Int @id @default(autoincrement()) - value Int - @@allow('read', value >-1) - } - ` - ); - const db = enhance(); - await expect(db.model.check({ operation: 'read' })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy(); - await expect(db.model.check({ operation: 'read', where: { value: 2 } })).toResolveTruthy(); - }); - - it('supports policy delegation simple', async () => { - const { enhance } = await load( - ` - model User { - id Int @id @default(autoincrement()) - foo Foo[] - } - - model Foo { - id Int @id @default(autoincrement()) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - model Model? - @@allow('read', auth().id == ownerId) - @@allow('create', auth().id != ownerId) - @@allow('update', auth() == owner) - } - - model Model { - id Int @id @default(autoincrement()) - foo Foo @relation(fields: [fooId], references: [id]) - fooId Int @unique - @@allow('all', check(foo)) - } - `, - { preserveTsFiles: true } - ); - - await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read' })).toResolveTruthy(); - - await expect(enhance().model.check({ operation: 'create' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create' })).toResolveTruthy(); - - await expect(enhance().model.check({ operation: 'update' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update' })).toResolveTruthy(); - - await expect(enhance().model.check({ operation: 'delete' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'delete' })).toResolveFalsy(); - }); - - it('supports policy delegation explicit', async () => { - const { enhance } = await load( - ` - model Foo { - id Int @id @default(autoincrement()) - model Model? - @@allow('all', true) - @@deny('update', true) - } - - model Model { - id Int @id @default(autoincrement()) - foo Foo @relation(fields: [fooId], references: [id]) - fooId Int @unique - @@allow('read', check(foo, 'update')) - } - `, - { preserveTsFiles: true } - ); - - await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy(); - }); - - it('supports policy delegation combined', async () => { - const { enhance } = await load( - ` - model User { - id Int @id @default(autoincrement()) - foo Foo[] - } - - model Foo { - id Int @id @default(autoincrement()) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - model Model? - @@allow('read', auth().id == ownerId) - @@allow('create', auth().id != ownerId) - @@allow('update', auth() == owner) - } - - model Model { - id Int @id @default(autoincrement()) - foo Foo @relation(fields: [fooId], references: [id]) - fooId Int @unique - value Int - @@allow('all', check(foo) && value > 0) - @@deny('update', check(foo) && value == 1) - } - `, - { preserveTsFiles: true } - ); - - await expect(enhance().model.check({ operation: 'read' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read', where: { value: 1 } })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'read', where: { value: 0 } })).toResolveFalsy(); - - await expect(enhance().model.check({ operation: 'create' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create', where: { value: 1 } })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'create', where: { value: 0 } })).toResolveFalsy(); - - await expect(enhance().model.check({ operation: 'update' })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update' })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update', where: { value: 2 } })).toResolveTruthy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update', where: { value: 0 } })).toResolveFalsy(); - await expect(enhance({ id: 1 }).model.check({ operation: 'update', where: { value: 1 } })).toResolveFalsy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts b/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts deleted file mode 100644 index 1d907a4f2..000000000 --- a/tests/integration/tests/enhancements/with-policy/client-extensions.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: client extensions', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(async () => { - process.chdir(origDir); - }); - - it('all model new method', async () => { - const { prisma, enhanceRaw, prismaModule } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('read', value > 0) - } - ` - ); - - await prisma.model.create({ data: { value: 0 } }); - await prisma.model.create({ data: { value: 1 } }); - await prisma.model.create({ data: { value: 2 } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-getAll', - model: { - $allModels: { - async getAll(this: T, args?: any) { - const context = prismaModule.getExtensionContext(this); - const r = await (context as any).findMany(args); - console.log('getAll result:', r); - return r; - }, - }, - }, - }); - }); - - await expect(prisma.$extends(ext).model.getAll()).resolves.toHaveLength(3); - await expect(enhanceRaw(prisma.$extends(ext)).model.getAll()).resolves.toHaveLength(2); - await expect(enhanceRaw(prisma).$extends(ext).model.getAll()).resolves.toHaveLength(2); - }); - - it('one model new method', async () => { - const { prisma, enhanceRaw, prismaModule } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('read', value > 0) - } - ` - ); - - await prisma.model.create({ data: { value: 0 } }); - await prisma.model.create({ data: { value: 1 } }); - await prisma.model.create({ data: { value: 2 } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-getAll', - model: { - model: { - async getAll(this: T, args?: any) { - const context = prismaModule.getExtensionContext(this); - const r = await (context as any).findMany(args); - return r; - }, - }, - }, - }); - }); - - await expect(prisma.$extends(ext).model.getAll()).resolves.toHaveLength(3); - await expect(enhanceRaw(prisma.$extends(ext)).model.getAll()).resolves.toHaveLength(2); - await expect(enhanceRaw(prisma).$extends(ext).model.getAll()).resolves.toHaveLength(2); - }); - - it('add client method', async () => { - const { prisma, prismaModule, enhanceRaw } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('read', value > 0) - } - ` - ); - - let logged = false; - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-log', - client: { - $log: (s: string) => { - console.log(s); - logged = true; - }, - }, - }); - }); - - enhanceRaw(prisma).$extends(ext).$log('abc'); - expect(logged).toBeTruthy(); - - logged = false; - enhanceRaw(prisma.$extends(ext)).$log('abc'); - expect(logged).toBeTruthy(); - }); - - it('query override one model', async () => { - const { prisma, prismaModule, enhanceRaw } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - x Int - y Int - - @@allow('read', x > 0) - } - ` - ); - - await prisma.model.create({ data: { x: 0, y: 100 } }); - await prisma.model.create({ data: { x: 1, y: 200 } }); - await prisma.model.create({ data: { x: 2, y: 300 } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-queryOverride', - query: { - model: { - async findMany({ args, query }: any) { - args.where = { ...args.where, y: { lt: 300 } }; - return query(args); - }, - }, - }, - }); - }); - - await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toHaveLength(1); - await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toHaveLength(1); - }); - - it('query override all models', async () => { - const { prisma, prismaModule, enhanceRaw } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - x Int - y Int - - @@allow('read', x > 0) - } - ` - ); - - await prisma.model.create({ data: { x: 0, y: 100 } }); - await prisma.model.create({ data: { x: 1, y: 200 } }); - await prisma.model.create({ data: { x: 2, y: 300 } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-queryOverride', - query: { - $allModels: { - async findMany({ args, query }: any) { - args.where = { ...args.where, y: { lt: 300 } }; - console.log('findMany args:', args); - return query(args); - }, - }, - }, - }); - }); - - await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toHaveLength(1); - await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toHaveLength(1); - }); - - it('query override all operations', async () => { - const { prisma, prismaModule, enhanceRaw } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - x Int - y Int - - @@allow('read', x > 0) - } - ` - ); - - await prisma.model.create({ data: { x: 0, y: 100 } }); - await prisma.model.create({ data: { x: 1, y: 200 } }); - await prisma.model.create({ data: { x: 2, y: 300 } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-queryOverride', - query: { - model: { - async $allOperations({ operation, args, query }: any) { - args.where = { ...args.where, y: { lt: 300 } }; - console.log(`${operation} args:`, args); - return query(args); - }, - }, - }, - }); - }); - - await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toHaveLength(1); - await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toHaveLength(1); - }); - - it('query override everything', async () => { - const { prisma, prismaModule, enhanceRaw } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - x Int - y Int - - @@allow('read', x > 0) - } - ` - ); - - await prisma.model.create({ data: { x: 0, y: 100 } }); - await prisma.model.create({ data: { x: 1, y: 200 } }); - await prisma.model.create({ data: { x: 2, y: 300 } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-queryOverride', - query: { - async $allOperations({ operation, args, query }: any) { - args.where = { ...args.where, y: { lt: 300 } }; - console.log(`${operation} args:`, args); - return query(args); - }, - }, - }); - }); - - await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toHaveLength(1); - await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toHaveLength(1); - }); - - it('result mutation', async () => { - const { prisma, prismaModule, enhanceRaw } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('read', value > 0) - } - ` - ); - - await prisma.model.create({ data: { value: 0 } }); - await prisma.model.create({ data: { value: 1 } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-resultMutation', - query: { - model: { - async findMany({ args, query }: any) { - const r: any = await query(args); - for (let i = 0; i < r.length; i++) { - r[i].value = r[i].value + 1; - } - return r; - }, - }, - }, - }); - }); - - const expected = [expect.objectContaining({ value: 2 })]; - await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toEqual(expected); - await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toEqual(expected); - }); - - it('result custom fields', async () => { - const { prisma, prismaModule, enhanceRaw } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('read', value > 0) - } - ` - ); - - await prisma.model.create({ data: { value: 0 } }); - await prisma.model.create({ data: { value: 1 } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - name: 'prisma-extension-resultNewFields', - result: { - model: { - doubleValue: { - needs: { value: true }, - compute(m: any) { - return m.value * 2; - }, - }, - }, - }, - }); - }); - - const expected = [expect.objectContaining({ doubleValue: 2 })]; - await expect(enhanceRaw(prisma.$extends(ext)).model.findMany()).resolves.toEqual(expected); - await expect(enhanceRaw(prisma).$extends(ext).model.findMany()).resolves.toEqual(expected); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/connect-disconnect.test.ts b/tests/integration/tests/enhancements/with-policy/connect-disconnect.test.ts deleted file mode 100644 index 7bc4a9ed9..000000000 --- a/tests/integration/tests/enhancements/with-policy/connect-disconnect.test.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: connect-disconnect', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - const modelToMany = ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - value Int @default(0) - - @@deny('read', value < 0) - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m1 M1? @relation(fields: [m1Id], references:[id]) - m1Id String? - m3 M3[] - - @@allow('read,create', true) - @@allow('update', !deleted) - } - - model M3 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m2 M2? @relation(fields: [m2Id], references:[id]) - m2Id String? - - @@allow('read,create', true) - @@allow('update', !deleted) - } - `; - - it('simple to-many', async () => { - const { enhance, prisma } = await loadSchema(modelToMany); - - const db = enhance(); - - // m1-1 -> m2-1 - await db.m2.create({ data: { id: 'm2-1', value: 1, deleted: false } }); - await db.m1.create({ - data: { - id: 'm1-1', - m2: { - connect: { id: 'm2-1' }, - }, - }, - }); - // mark m2-1 deleted - await prisma.m2.update({ where: { id: 'm2-1' }, data: { deleted: true } }); - // disconnect denied because of violation of m2's update rule - await expect( - db.m1.update({ - where: { id: 'm1-1' }, - data: { - m2: { - disconnect: { id: 'm2-1' }, - }, - }, - }) - ).toBeRejectedByPolicy(); - // reset m2-1 delete - await prisma.m2.update({ where: { id: 'm2-1' }, data: { deleted: false } }); - // disconnect allowed - await db.m1.update({ - where: { id: 'm1-1' }, - data: { - m2: { - disconnect: { id: 'm2-1' }, - }, - }, - }); - - // connect during create denied - await db.m2.create({ data: { id: 'm2-2', value: 1, deleted: true } }); - await expect( - db.m1.create({ - data: { - m2: { - connect: { id: 'm2-2' }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // mixed create and connect - await db.m2.create({ data: { id: 'm2-3', value: 1, deleted: false } }); - await db.m1.create({ - data: { - m2: { - connect: { id: 'm2-3' }, - create: { value: 1, deleted: false }, - }, - }, - }); - - await db.m2.create({ data: { id: 'm2-4', value: 1, deleted: true } }); - await expect( - db.m1.create({ - data: { - m2: { - connect: { id: 'm2-4' }, - create: { value: 1, deleted: false }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // connectOrCreate - await db.m1.create({ - data: { - m2: { - connectOrCreate: { - where: { id: 'm2-5' }, - create: { value: 1 }, - }, - }, - }, - }); - - await db.m2.create({ data: { id: 'm2-6', value: 1, deleted: true } }); - await expect( - db.m1.create({ - data: { - m2: { - connectOrCreate: { - where: { id: 'm2-6' }, - create: { value: 1 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // // connect from m2 to m1, require m1 to be readable - // await db.m2.create({ data: { id: 'm2-7', value: 1 } }); - // await prisma.m1.create({ data: { id: 'm1-2', value: -1 } }); - // // connect is denied because m1 is not readable - // await expect( - // db.m2.update({ - // where: { id: 'm2-7' }, - // data: { - // m1: { - // connect: { id: 'm1-2' }, - // }, - // }, - // }) - // ).toBeRejectedByPolicy(); - }); - - it('nested to-many', async () => { - const { enhance } = await loadSchema(modelToMany); - - const db = enhance(); - - await db.m3.create({ data: { id: 'm3-1', value: 1, deleted: false } }); - await expect( - db.m1.create({ - data: { - id: 'm1-1', - m2: { - create: { - value: 1, - m3: { connect: { id: 'm3-1' } }, - }, - }, - }, - }) - ).toResolveTruthy(); - - await db.m3.create({ data: { id: 'm3-2', value: 1, deleted: true } }); - await expect( - db.m1.create({ - data: { - m2: { - create: { - value: 1, - m3: { connect: { id: 'm3-2' } }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); - - const modelToOne = ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m1 M1? @relation(fields: [m1Id], references:[id]) - m1Id String? @unique - - @@allow('read,create', true) - @@allow('update', !deleted) - } - `; - - it('to-one', async () => { - const { enhance, prisma } = await loadSchema(modelToOne); - - const db = enhance(); - - await db.m2.create({ data: { id: 'm2-1', value: 1, deleted: false } }); - await db.m1.create({ - data: { - id: 'm1-1', - m2: { - connect: { id: 'm2-1' }, - }, - }, - }); - await prisma.m2.update({ where: { id: 'm2-1' }, data: { deleted: true } }); - await expect( - db.m1.update({ - where: { id: 'm1-1' }, - data: { - m2: { - disconnect: { id: 'm2-1' }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await prisma.m2.update({ where: { id: 'm2-1' }, data: { deleted: false } }); - await db.m1.update({ - where: { id: 'm1-1' }, - data: { - m2: { - disconnect: true, - }, - }, - }); - - await db.m2.create({ data: { id: 'm2-2', value: 1, deleted: true } }); - await expect( - db.m1.create({ - data: { - m2: { - connect: { id: 'm2-2' }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // connectOrCreate - await db.m1.create({ - data: { - m2: { - connectOrCreate: { - where: { id: 'm2-3' }, - create: { value: 1 }, - }, - }, - }, - }); - - await db.m2.create({ data: { id: 'm2-4', value: 1, deleted: true } }); - await expect( - db.m1.create({ - data: { - m2: { - connectOrCreate: { - where: { id: 'm2-4' }, - create: { value: 1 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); - - const modelImplicitManyToMany = ` - model M1 { - id String @id @default(uuid()) - value Int @default(0) - m2 M2[] - - @@deny('read', value < 0) - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m1 M1[] - - @@deny('read', value < 0) - @@allow('read,create', true) - @@allow('update', !deleted) - } - `; - - it('implicit many-to-many', async () => { - const { enhance, prisma } = await loadSchema(modelImplicitManyToMany); - - const db = enhance(); - - // await prisma.m1.create({ data: { id: 'm1-1', value: 1 } }); - // await prisma.m2.create({ data: { id: 'm2-1', value: 1 } }); - // await expect( - // db.m1.update({ - // where: { id: 'm1-1' }, - // data: { m2: { connect: { id: 'm2-1' } } }, - // }) - // ).toResolveTruthy(); - - await prisma.m1.create({ data: { id: 'm1-2', value: 1 } }); - await prisma.m2.create({ data: { id: 'm2-2', value: 1, deleted: true } }); - // m2-2 not updatable - await expect( - db.m1.update({ - where: { id: 'm1-2' }, - data: { m2: { connect: { id: 'm2-2' } } }, - }) - ).toBeRejectedByPolicy(); - - // await prisma.m1.create({ data: { id: 'm1-3', value: -1 } }); - // await prisma.m2.create({ data: { id: 'm2-3', value: 1 } }); - // // m1-3 not readable - // await expect( - // db.m2.update({ - // where: { id: 'm2-3' }, - // data: { m1: { connect: { id: 'm1-3' } } }, - // }) - // ).toBeRejectedByPolicy(); - }); - - const modelExplicitManyToMany = ` - model M1 { - id String @id @default(uuid()) - value Int @default(0) - m2 M1OnM2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m1 M1OnM2[] - - @@allow('read,create', true) - } - - model M1OnM2 { - m1 M1 @relation(fields: [m1Id], references: [id]) - m1Id String - m2 M2 @relation(fields: [m2Id], references: [id]) - m2Id String - - @@id([m1Id, m2Id]) - @@allow('read', true) - @@allow('create', !m2.deleted) - } - `; - - it('explicit many-to-many', async () => { - const { enhance, prisma } = await loadSchema(modelExplicitManyToMany); - - const db = enhance(); - - await prisma.m1.create({ data: { id: 'm1-1', value: 1 } }); - await prisma.m2.create({ data: { id: 'm2-1', value: 1 } }); - await expect( - db.m1OnM2.create({ - data: { m1: { connect: { id: 'm1-1' } }, m2: { connect: { id: 'm2-1' } } }, - }) - ).toResolveTruthy(); - - await prisma.m1.create({ data: { id: 'm1-2', value: 1 } }); - await prisma.m2.create({ data: { id: 'm2-2', value: 1, deleted: true } }); - await expect( - db.m1OnM2.create({ - data: { m1: { connect: { id: 'm1-2' } }, m2: { connect: { id: 'm2-2' } } }, - }) - ).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/create-many-and-return.test.ts b/tests/integration/tests/enhancements/with-policy/create-many-and-return.test.ts deleted file mode 100644 index f0739ef80..000000000 --- a/tests/integration/tests/enhancements/with-policy/create-many-and-return.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Test API createManyAndReturn', () => { - it('model-level policies', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - posts Post[] - level Int - - @@allow('read', level > 0) - } - - model Post { - id Int @id @default(autoincrement()) - title String - published Boolean @default(false) - userId Int - user User @relation(fields: [userId], references: [id]) - - @@allow('read', published) - @@allow('create', contains(title, 'hello')) - } - ` - ); - - await prisma.user.createMany({ - data: [ - { id: 1, level: 1 }, - { id: 2, level: 0 }, - ], - }); - - const db = enhance(); - - // create rule violation - await expect( - db.post.createManyAndReturn({ - data: [{ title: 'foo', userId: 1 }], - }) - ).toBeRejectedByPolicy(); - - // success - let r = await db.post.createManyAndReturn({ - data: [{ id: 1, title: 'hello1', userId: 1, published: true }], - }); - expect(r.length).toBe(1); - - // read-back check - await expect( - db.post.createManyAndReturn({ - data: [ - { id: 2, title: 'hello2', userId: 1, published: true }, - { id: 3, title: 'hello3', userId: 1, published: false }, - ], - }) - ).toBeRejectedByPolicy(['result is not allowed to be read back']); - await expect(prisma.post.findMany()).resolves.toHaveLength(3); - - // return relation - await prisma.post.deleteMany(); - r = await db.post.createManyAndReturn({ - include: { user: true }, - data: [{ id: 1, title: 'hello1', userId: 1, published: true }], - }); - expect(r[0]).toMatchObject({ user: { id: 1 } }); - - // relation filtered - await prisma.post.deleteMany(); - await expect( - db.post.createManyAndReturn({ - include: { user: true }, - data: [{ id: 1, title: 'hello1', userId: 2, published: true }], - }) - ).toBeRejectedByPolicy(['result is not allowed to be read back']); - await expect(prisma.post.findMany()).resolves.toHaveLength(1); - }); - - it('field-level policies', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - title String @allow('read', published) - published Boolean @default(false) - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - // create should succeed but one result's title field can't be read back - const r = await db.post.createManyAndReturn({ - data: [ - { title: 'post1', published: true }, - { title: 'post2', published: false }, - ], - }); - - expect(r.length).toBe(2); - expect(r[0].title).toBeTruthy(); - expect(r[1].title).toBeUndefined(); - - // check posts are created - await expect(prisma.post.findMany()).resolves.toHaveLength(2); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/cross-model-field-comparison.test.ts b/tests/integration/tests/enhancements/with-policy/cross-model-field-comparison.test.ts deleted file mode 100644 index 1ffb9e35c..000000000 --- a/tests/integration/tests/enhancements/with-policy/cross-model-field-comparison.test.ts +++ /dev/null @@ -1,1029 +0,0 @@ -import { loadModel, loadSchema } from '@zenstackhq/testtools'; - -describe('Cross-model field comparison', () => { - it('to-one relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - profile Profile @relation(fields: [profileId], references: [id]) - profileId Int @unique - age Int - - @@allow('read', true) - @@allow('create,update,delete', age == profile.age) - @@deny('update', future().age < future().profile.age && age > 0) - } - - model Profile { - id Int @id - age Int - user User? - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - const reset = async () => { - await prisma.user.deleteMany(); - await prisma.profile.deleteMany(); - }; - - // create - await expect( - db.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } } }) - ).toBeRejectedByPolicy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).toResolveNull(); - await expect( - db.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } } }) - ).toResolveTruthy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); - await reset(); - - // createMany - await expect( - db.user.createMany({ data: [{ id: 1, age: 18, profile: { create: { id: 1, age: 20 } } }] }) - ).toBeRejectedByPolicy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).toResolveNull(); - await expect( - db.user.createMany({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } } }) - ).toResolveTruthy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); - await reset(); - - // TODO: cross-model field comparison is not supported for read rules yet - // // read - // await prisma.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.findMany()).resolves.toHaveLength(1); - // await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - // await expect(db.user.findUnique({ where: { id: 1 } })).toResolveNull(); - // await expect(db.user.findMany()).resolves.toHaveLength(0); - // await reset(); - - // update - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } } }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 20 } })).toResolveTruthy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ age: 20 }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 18 } })).toBeRejectedByPolicy(); - await reset(); - - // post update - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } } }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 15 } })).toBeRejectedByPolicy(); - await expect(db.user.update({ where: { id: 1 }, data: { age: 20 } })).toResolveTruthy(); - await reset(); - - // upsert - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } } }); - await expect( - db.user.upsert({ where: { id: 1 }, create: { id: 1, age: 25 }, update: { age: 25 } }) - ).toBeRejectedByPolicy(); - await expect( - db.user.upsert({ - where: { id: 2 }, - create: { id: 2, age: 18, profile: { create: { id: 2, age: 25 } } }, - update: { age: 25 }, - }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect( - db.user.upsert({ where: { id: 1 }, create: { id: 1, age: 25 }, update: { age: 25 } }) - ).toResolveTruthy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ age: 25 }); - await expect( - db.user.upsert({ - where: { id: 2 }, - create: { id: 2, age: 25, profile: { create: { id: 2, age: 25 } } }, - update: { age: 25 }, - }) - ).toResolveTruthy(); - await expect(prisma.user.findMany()).resolves.toHaveLength(2); - await reset(); - - // updateMany - await prisma.user.create({ 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 prisma.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(prisma.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ age: 18 }); - await expect(prisma.user.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ age: 30 }); - await reset(); - - // delete - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } } }); - await expect(db.user.delete({ where: { id: 1 } })).toBeRejectedByPolicy(); - await expect(prisma.user.findMany()).resolves.toHaveLength(1); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect(db.user.delete({ where: { id: 1 } })).toResolveTruthy(); - await expect(prisma.user.findMany()).resolves.toHaveLength(0); - await reset(); - - // deleteMany - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } } }); - await expect(db.user.deleteMany()).resolves.toMatchObject({ count: 0 }); - await prisma.user.create({ data: { id: 2, age: 25, profile: { create: { id: 2, age: 25 } } } }); - // one of the two is deletable - await expect(db.user.deleteMany()).resolves.toMatchObject({ count: 1 }); - await expect(prisma.user.findMany()).resolves.toHaveLength(1); - }); - - it('nested inside to-one relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - profile Profile? - age Int - - @@allow('all', true) - } - - model Profile { - id Int @id - age Int - user User? @relation(fields: [userId], references: [id]) - userId Int? @unique - - @@allow('read', true) - @@allow('create,update,delete', user == null || age == user.age) - @@deny('update', future().user != null && future().age < future().user.age && age > 0) - } - ` - ); - - const db = enhance(); - - const reset = async () => { - await prisma.profile.deleteMany(); - await prisma.user.deleteMany(); - }; - - // create - await expect( - db.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } } }) - ).toBeRejectedByPolicy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).toResolveNull(); - await expect( - db.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } } }) - ).toResolveTruthy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); - await reset(); - - // TODO: cross-model field comparison is not supported for read rules yet - // // read - // await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } } }); - // await expect(db.user.findUnique({ where: { id: 1 }, include: { profile: true } })).resolves.toMatchObject({ - // age: 18, - // profile: expect.objectContaining({ age: 18 }), - // }); - // await expect(db.user.findMany({ include: { profile: true } })).resolves.toEqual( - // expect.arrayContaining([ - // expect.objectContaining({ - // age: 18, - // profile: expect.objectContaining({ age: 18 }), - // }), - // ]) - // ); - // await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - // let r = await db.user.findUnique({ where: { id: 1 }, include: { profile: true } }); - // expect(r.profile).toBeUndefined(); - // r = await db.user.findMany({ include: { profile: true } }); - // expect(r[0].profile).toBeUndefined(); - // await reset(); - - // update - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } } }); - await expect( - db.user.update({ where: { id: 1 }, data: { profile: { update: { age: 20 } } } }) - ).toResolveTruthy(); - const r = await prisma.user.findUnique({ where: { id: 1 }, include: { profile: true } }); - expect(r.profile).toMatchObject({ age: 20 }); - await expect( - db.user.update({ where: { id: 1 }, data: { profile: { update: { age: 18 } } } }) - ).toBeRejectedByPolicy(); - await reset(); - - // post update - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 18 } } } }); - await expect( - db.user.update({ where: { id: 1 }, data: { profile: { update: { age: 15 } } } }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ where: { id: 1 }, data: { profile: { update: { age: 20 } } } }) - ).toResolveTruthy(); - await reset(); - - // upsert - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } } }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { - profile: { - upsert: { - create: { id: 1, age: 25 }, - update: { age: 25 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { - profile: { - upsert: { - create: { id: 1, age: 25 }, - update: { age: 25 }, - }, - }, - }, - }) - ).toResolveTruthy(); - await prisma.user.create({ data: { id: 2, age: 18 } }); - await expect( - db.user.update({ - where: { id: 2 }, - data: { - profile: { - upsert: { - create: { id: 2, age: 25 }, - update: { age: 25 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ - where: { id: 2 }, - data: { - profile: { - upsert: { - create: { id: 2, age: 18 }, - update: { age: 25 }, - }, - }, - }, - }) - ).toResolveTruthy(); - await reset(); - - // delete - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } } }); - await expect(db.user.update({ where: { id: 1 }, data: { profile: { delete: true } } })).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect(db.user.update({ where: { id: 1 }, data: { profile: { delete: true } } })).toResolveTruthy(); - await expect(await prisma.profile.findMany()).toHaveLength(0); - await reset(); - - // connect/disconnect - await prisma.user.create({ data: { id: 1, age: 18, profile: { create: { id: 1, age: 20 } } } }); - await expect( - db.user.update({ where: { id: 1 }, data: { profile: { disconnect: true } } }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect(db.user.update({ where: { id: 1 }, data: { profile: { disconnect: true } } })).toResolveTruthy(); - await prisma.user.create({ data: { id: 2, age: 25 } }); - await expect( - db.user.update({ where: { id: 2 }, data: { profile: { connect: { id: 1 } } } }) - ).toBeRejectedByPolicy(); - await prisma.user.create({ data: { id: 3, age: 20 } }); - await expect(db.user.update({ where: { id: 3 }, data: { profile: { connect: { id: 1 } } } })).toResolveTruthy(); - await expect(prisma.profile.findFirst()).resolves.toMatchObject({ userId: 3 }); - await reset(); - }); - - it('to-many relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - profiles Profile[] - age Int - - @@allow('read', true) - @@allow('create,update,delete', profiles![this.age == age]) - @@deny('update', future().profiles?[this.age < age]) - } - - model Profile { - id Int @id - age Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - - @@allow('all', true) - } - `, - { preserveTsFiles: true } - ); - - const db = enhance(); - - const reset = async () => { - await prisma.user.deleteMany(); - }; - - // create - await expect( - db.user.create({ data: { id: 1, age: 18, profiles: { create: [{ id: 1, age: 20 }] } } }) - ).toBeRejectedByPolicy(); - await expect( - db.user.create({ - data: { - id: 1, - age: 18, - profiles: { - createMany: { - data: [ - { id: 1, age: 18 }, - { id: 2, age: 20 }, - ], - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.create({ data: { id: 1, age: 18, profiles: { create: [{ id: 1, age: 20 }] } } }) - ).toBeRejectedByPolicy(); - await expect( - db.user.create({ - data: { - id: 1, - age: 18, - profiles: { - createMany: { - data: [ - { id: 1, age: 18 }, - { id: 2, age: 18 }, - ], - }, - }, - }, - }) - ).toResolveTruthy(); - await expect( - db.user.create({ - data: { id: 2, age: 18 }, - }) - ).toResolveTruthy(); - await reset(); - - // createMany - await expect( - db.user.createMany({ - data: [ - { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } }, - { id: 2, age: 18, profiles: { create: { id: 2, age: 20 } } }, - ], - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.createMany({ - data: [ - { id: 1, age: 18, profiles: { create: { id: 1, age: 18 } } }, - { id: 2, age: 19, profiles: { create: { id: 2, age: 19 } } }, - ], - }) - ).resolves.toEqual({ count: 2 }); - await reset(); - - // TODO: cross-model field comparison is not supported for read rules yet - // // read - // await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 18 } } } }); - // await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); - // await expect(db.user.findMany()).resolves.toHaveLength(1); - // await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - // await expect(db.user.findUnique({ where: { id: 1 } })).toResolveNull(); - // await expect(db.user.findMany()).resolves.toHaveLength(0); - // await reset(); - - // update - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 18 } } } }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 20 } })).toResolveTruthy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ age: 20 }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 18 } })).toBeRejectedByPolicy(); - await reset(); - - // post update - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 18 } } } }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 15 } })).toBeRejectedByPolicy(); - await expect(db.user.update({ where: { id: 1 }, data: { age: 20 } })).toResolveTruthy(); - await reset(); - - // upsert - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } } }); - await expect( - db.user.upsert({ where: { id: 1 }, create: { id: 1, age: 25 }, update: { age: 25 } }) - ).toBeRejectedByPolicy(); - await expect( - db.user.upsert({ - where: { id: 2 }, - create: { id: 2, age: 18, profiles: { create: { id: 2, age: 25 } } }, - update: { age: 25 }, - }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect( - db.user.upsert({ where: { id: 1 }, create: { id: 1, age: 25 }, update: { age: 25 } }) - ).toResolveTruthy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ age: 25 }); - await expect( - db.user.upsert({ - where: { id: 2 }, - create: { id: 2, age: 25, profiles: { create: { id: 2, age: 25 } } }, - update: { age: 25 }, - }) - ).toResolveTruthy(); - await expect(prisma.user.findMany()).resolves.toHaveLength(2); - await reset(); - - // updateMany - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } } }); - // non updatable - await expect(db.user.updateMany({ data: { age: 18 } })).resolves.toMatchObject({ count: 0 }); - await prisma.user.create({ data: { id: 2, age: 25, profiles: { 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(prisma.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ age: 18 }); - await expect(prisma.user.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ age: 30 }); - await reset(); - - // delete - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } } }); - await expect(db.user.delete({ where: { id: 1 } })).toBeRejectedByPolicy(); - await expect(prisma.user.findMany()).resolves.toHaveLength(1); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect(db.user.delete({ where: { id: 1 } })).toResolveTruthy(); - await expect(prisma.user.findMany()).resolves.toHaveLength(0); - await reset(); - - // deleteMany - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } } }); - await expect(db.user.deleteMany()).resolves.toMatchObject({ count: 0 }); - await prisma.user.create({ data: { id: 2, age: 25, profiles: { create: { id: 2, age: 25 } } } }); - // one of the two is deletable - await expect(db.user.deleteMany()).resolves.toMatchObject({ count: 1 }); - await expect(prisma.user.findMany()).resolves.toHaveLength(1); - }); - - it('nested inside to-many relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - profiles Profile[] - age Int - - @@allow('all', true) - } - - model Profile { - id Int @id - age Int - user User? @relation(fields: [userId], references: [id]) - userId Int? @unique - - @@allow('read', true) - @@allow('create,update,delete', user == null || age == user.age) - @@deny('update', future().user != null && future().age < future().user.age && age > 0) - } - ` - ); - - const db = enhance(); - - const reset = async () => { - await prisma.profile.deleteMany(); - await prisma.user.deleteMany(); - }; - - // create - await expect( - db.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } } }) - ).toBeRejectedByPolicy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).toResolveNull(); - await expect( - db.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 18 } } } }) - ).toResolveTruthy(); - await expect(prisma.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); - await reset(); - - // TODO: cross-model field comparison is not supported for read rules yet - // // read - // await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 18 } } } }); - // await expect(db.user.findUnique({ where: { id: 1 }, include: { profiles: true } })).resolves.toMatchObject({ - // age: 18, - // profiles: [expect.objectContaining({ age: 18 })], - // }); - // await expect(db.user.findMany({ include: { profiles: true } })).resolves.toEqual( - // expect.arrayContaining([ - // expect.objectContaining({ - // age: 18, - // profiles: [expect.objectContaining({ age: 18 })], - // }), - // ]) - // ); - // await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - // let r = await db.user.findUnique({ where: { id: 1 }, include: { profiles: true } }); - // expect(r.profiles).toHaveLength(0); - // r = await db.user.findMany({ include: { profiles: true } }); - // expect(r[0].profiles).toHaveLength(0); - // await reset(); - - // update - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 18 } } } }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { profiles: { update: { where: { id: 1 }, data: { age: 20 } } } }, - }) - ).toResolveTruthy(); - let r = await prisma.user.findUnique({ where: { id: 1 }, include: { profiles: true } }); - expect(r.profiles[0]).toMatchObject({ age: 20 }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { profiles: { update: { where: { id: 1 }, data: { age: 18 } } } }, - }) - ).toBeRejectedByPolicy(); - await reset(); - - // post update - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 18 } } } }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { profiles: { update: { where: { id: 1 }, data: { age: 15 } } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { profiles: { update: { where: { id: 1 }, data: { age: 20 } } } }, - }) - ).toResolveTruthy(); - await reset(); - - // upsert - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } } }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { - profiles: { - upsert: { - where: { id: 1 }, - create: { id: 1, age: 25 }, - update: { age: 25 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { - profiles: { - upsert: { - where: { id: 1 }, - create: { id: 1, age: 25 }, - update: { age: 25 }, - }, - }, - }, - }) - ).toResolveTruthy(); - await prisma.user.create({ data: { id: 2, age: 18 } }); - await expect( - db.user.update({ - where: { id: 2 }, - data: { - profiles: { - upsert: { - where: { id: 2 }, - create: { id: 2, age: 25 }, - update: { age: 25 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ - where: { id: 2 }, - data: { - profiles: { - upsert: { - where: { id: 2 }, - create: { id: 2, age: 18 }, - update: { age: 25 }, - }, - }, - }, - }) - ).toResolveTruthy(); - await reset(); - - // delete - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } } }); - await expect( - db.user.update({ where: { id: 1 }, data: { profiles: { delete: { id: 1 } } } }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect(db.user.update({ where: { id: 1 }, data: { profiles: { delete: { id: 1 } } } })).toResolveTruthy(); - await expect(await prisma.profile.findMany()).toHaveLength(0); - await reset(); - - // connect/disconnect - await prisma.user.create({ data: { id: 1, age: 18, profiles: { create: { id: 1, age: 20 } } } }); - await expect( - db.user.update({ where: { id: 1 }, data: { profiles: { disconnect: { id: 1 } } } }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - await expect( - db.user.update({ where: { id: 1 }, data: { profiles: { disconnect: { id: 1 } } } }) - ).toResolveTruthy(); - await prisma.user.create({ data: { id: 2, age: 25 } }); - await expect( - db.user.update({ where: { id: 2 }, data: { profiles: { connect: { id: 1 } } } }) - ).toBeRejectedByPolicy(); - await prisma.user.create({ data: { id: 3, age: 20 } }); - await expect( - db.user.update({ where: { id: 3 }, data: { profiles: { connect: { id: 1 } } } }) - ).toResolveTruthy(); - await expect(prisma.profile.findFirst()).resolves.toMatchObject({ userId: 3 }); - await reset(); - }); - - it('field-level simple', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - profile Profile @relation(fields: [profileId], references: [id]) - profileId Int @unique - age Int @allow('read', age == profile.age) @allow('update', age > profile.age) - level Int - - @@allow('all', true) - } - - model Profile { - id Int @id - age Int - user User? - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - // read - await prisma.user.create({ data: { id: 1, age: 18, level: 1, profile: { create: { id: 1, age: 20 } } } }); - let r = await db.user.findUnique({ where: { id: 1 } }); - expect(r.age).toBeUndefined(); - r = await db.user.findUnique({ where: { id: 1 }, select: { age: true } }); - expect(r.age).toBeUndefined(); - - // update - await expect(db.user.update({ where: { id: 1 }, data: { age: 21 } })).toBeRejectedByPolicy(); - await expect(db.user.update({ where: { id: 1 }, data: { level: 2 } })).toResolveTruthy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 21 } }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 25 } })).toResolveTruthy(); - }); - - it('field-level read override', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - profile Profile @relation(fields: [profileId], references: [id]) - profileId Int @unique - age Int @allow('read', age == profile.age, true) - level Int - } - - model Profile { - id Int @id - age Int - user User? - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await prisma.user.create({ data: { id: 1, age: 18, level: 1, profile: { create: { id: 1, age: 20 } } } }); - let r = await db.user.findUnique({ where: { id: 1 } }); - expect(r).toBeNull(); - r = await db.user.findUnique({ where: { id: 1 }, select: { age: true } }); - expect(Object.keys(r).length).toBe(0); - await prisma.user.update({ where: { id: 1 }, data: { age: 20 } }); - r = await db.user.findUnique({ where: { id: 1 }, select: { age: true } }); - expect(r).toMatchObject({ age: 20 }); - }); - - it('field-level update override', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - profile Profile @relation(fields: [profileId], references: [id]) - profileId Int @unique - age Int @allow('update', age > profile.age, true) - level Int - @@allow('read', true) - } - - model Profile { - id Int @id - age Int - user User? - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await prisma.user.create({ data: { id: 1, age: 18, level: 1, profile: { create: { id: 1, age: 20 } } } }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 21 } })).toBeRejectedByPolicy(); - await expect(db.user.update({ where: { id: 1 }, data: { level: 2 } })).toBeRejectedByPolicy(); - await prisma.user.update({ where: { id: 1 }, data: { age: 21 } }); - await expect(db.user.update({ where: { id: 1 }, data: { age: 25 } })).toResolveTruthy(); - }); - - it('with auth case 1', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - permissions Permission[] - @@allow('all', true) - } - - model Permission { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - model String - level Int - @@allow('all', true) - } - - model Post { - id Int @id @default(autoincrement()) - title String - permission PostPermission? - - @@allow('read', true) - @@allow("create", auth().permissions?[model == 'Post' && level == this.permission.level]) - } - - model PostPermission { - id Int @id @default(autoincrement()) - post Post @relation(fields: [postId], references: [id]) - postId Int @unique - level Int - @@allow('all', true) - } - ` - ); - - await expect(enhance().post.create({ data: { title: 'P1' } })).toBeRejectedByPolicy(); - await expect( - enhance({ id: 1, permissions: [{ model: 'Foo', level: 1 }] }).post.create({ data: { title: 'P1' } }) - ).toBeRejectedByPolicy(); - await expect( - enhance({ id: 1, permissions: [{ model: 'Post', level: 1 }] }).post.create({ data: { title: 'P1' } }) - ).toBeRejectedByPolicy(); - await expect( - enhance({ id: 1, permissions: [{ model: 'Post', level: 1 }] }).post.create({ - data: { title: 'P1', permission: { create: { level: 1 } } }, - }) - ).toResolveTruthy(); - }); - - it('with auth case 2', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - teamMembership TeamMembership[] - @@allow('all', true) - } - - model Team { - id Int @id @default(autoincrement()) - permissions Permission[] - assets Asset[] - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - name String - team Team @relation(fields: [teamId], references: [id]) - teamId Int - @@allow('all', auth().teamMembership?[role.permissions?[name == 'ManageTeam' && teamId == this.teamId]]) - @@allow('read', true) - } - - model TeamMembership { - id Int @id @default(autoincrement()) - role TeamRole? - user User @relation(fields: [userId], references: [id]) - userId Int - @@allow('all', true) - } - - model TeamRole { - id Int @id @default(autoincrement()) - permissions Permission[] - membership TeamMembership @relation(fields: [membershipId], references: [id]) - membershipId Int @unique - @@allow('all', true) - } - - model Permission { - id Int @id @default(autoincrement()) - name String - team Team @relation(fields: [teamId], references: [id]) - teamId Int - role TeamRole @relation(fields: [roleId], references: [id]) - roleId Int - @@allow('all', true) - } - ` - ); - - const team1 = await prisma.team.create({ data: {} }); - const team2 = await prisma.team.create({ data: {} }); - - const user = await prisma.user.create({ - data: { - teamMembership: { - create: { - role: { - create: { - permissions: { create: [{ name: 'ManageTeam', team: { connect: { id: team1.id } } }] }, - }, - }, - }, - }, - }, - }); - - const asset = await prisma.asset.create({ - data: { name: 'Asset1', team: { connect: { id: team1.id } } }, - }); - - const dbTeam1 = enhance({ - id: user.id, - teamMembership: [{ role: { permissions: [{ name: 'ManageTeam', teamId: team1.id }] } }], - }); - await expect(dbTeam1.asset.update({ where: { id: asset.id }, data: { name: 'Asset2' } })).toResolveTruthy(); - - const dbTeam2 = enhance({ - id: user.id, - teamMembership: [{ role: { permissions: [{ name: 'ManageTeam', teamId: team2.id }] } }], - }); - await expect( - dbTeam2.asset.update({ where: { id: asset.id }, data: { name: 'Asset2' } }) - ).toBeRejectedByPolicy(); - }); - - it('with auth case 3', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - teamMembership TeamMembership[] - @@allow('all', true) - } - - model Team { - id Int @id @default(autoincrement()) - permissions Permission[] - assets Asset[] - @@allow('all', true) - } - - model Asset { - id Int @id @default(autoincrement()) - name String - team Team @relation(fields: [teamId], references: [id]) - teamId Int - @@allow('all', auth().teamMembership?[role.permissions?[name == 'ManageTeam' && team == this.team]]) - @@allow('read', true) - } - - model TeamMembership { - id Int @id @default(autoincrement()) - role TeamRole? - user User @relation(fields: [userId], references: [id]) - userId Int - @@allow('all', true) - } - - model TeamRole { - id Int @id @default(autoincrement()) - permissions Permission[] - membership TeamMembership @relation(fields: [membershipId], references: [id]) - membershipId Int @unique - @@allow('all', true) - } - - model Permission { - id Int @id @default(autoincrement()) - name String - team Team @relation(fields: [teamId], references: [id]) - teamId Int - role TeamRole @relation(fields: [roleId], references: [id]) - roleId Int - @@allow('all', true) - } - ` - ); - - const team1 = await prisma.team.create({ data: {} }); - const team2 = await prisma.team.create({ data: {} }); - - const user = await prisma.user.create({ - data: { - teamMembership: { - create: { - role: { - create: { - permissions: { create: [{ name: 'ManageTeam', team: { connect: { id: team1.id } } }] }, - }, - }, - }, - }, - }, - }); - - const asset = await prisma.asset.create({ - data: { name: 'Asset1', team: { connect: { id: team1.id } } }, - }); - - const dbTeam1 = enhance({ - id: user.id, - teamMembership: [{ role: { permissions: [{ name: 'ManageTeam', team: { id: team1.id } }] } }], - }); - await expect(dbTeam1.asset.update({ where: { id: asset.id }, data: { name: 'Asset2' } })).toResolveTruthy(); - - const dbTeam2 = enhance({ - id: user.id, - teamMembership: [{ role: { permissions: [{ name: 'ManageTeam', teamId: team2.id }] } }], - }); - await expect( - dbTeam2.asset.update({ where: { id: asset.id }, data: { name: 'Asset2' } }) - ).toBeRejectedByPolicy(); - }); - - it('with auth case 4', async () => { - await loadModel(` - model User { - id Int @id @default(autoincrement()) - foos Foo[] - bars Bar[] - @@allow('all', true) - } - - model Foo { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - bar Bar @relation(fields: [barId], references: [id]) - barId Int @unique - @@allow('all', bar.private && bar.user == auth()) - } - - model Bar { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - foo Foo? - private Boolean - @@allow('all', true) - } - `); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/currentModel.test.ts b/tests/integration/tests/enhancements/with-policy/currentModel.test.ts deleted file mode 100644 index 0b98314a4..000000000 --- a/tests/integration/tests/enhancements/with-policy/currentModel.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { loadModelWithError, loadSchema } from '@zenstackhq/testtools'; - -describe('currentModel tests', () => { - it('works in models', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('create', currentModel() == 'User') - } - - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentModel() == 'User') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with upper case', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('create', currentModel('upper') == 'USER') - } - - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentModel('upper') == 'Post') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with lower case', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('create', currentModel('lower') == 'user') - } - - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentModel('lower') == 'Post') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with capitalization', async () => { - const { enhance } = await loadSchema( - ` - model user { - id Int @id - @@allow('read', true) - @@allow('create', currentModel('capitalize') == 'User') - } - - model post { - id Int @id - @@allow('read', true) - @@allow('create', currentModel('capitalize') == 'post') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with uncapitalization', async () => { - const { enhance } = await loadSchema( - ` - model USER { - id Int @id - @@allow('read', true) - @@allow('create', currentModel('uncapitalize') == 'uSER') - } - - model POST { - id Int @id - @@allow('read', true) - @@allow('create', currentModel('uncapitalize') == 'POST') - } - ` - ); - - const db = enhance(); - await expect(db.USER.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.POST.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works when inherited from abstract base', async () => { - const { enhance } = await loadSchema( - ` - abstract model Base { - id Int @id - @@allow('read', true) - @@allow('create', currentModel() == 'User') - } - - model User extends Base { - } - - model Post extends Base { - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works when inherited from delegate base', async () => { - const { enhance } = await loadSchema( - ` - model Base { - id Int @id - type String - @@delegate(type) - - @@allow('read', true) - @@allow('create', currentModel() == 'User') - } - - model User extends Base { - } - - model Post extends Base { - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('complains when used outside policies', async () => { - await expect( - loadModelWithError( - ` - model User { - id String @default(currentModel()) - } - ` - ) - ).resolves.toContain('function "currentModel" is not allowed in the current context: DefaultValue'); - }); - - it('complains when casing argument is invalid', async () => { - await expect( - loadModelWithError( - ` - model User { - id String @id - @@allow('create', currentModel('foo') == 'User') - } - ` - ) - ).resolves.toContain('argument must be one of: "original", "upper", "lower", "capitalize", "uncapitalize"'); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/currentOperation.test.ts b/tests/integration/tests/enhancements/with-policy/currentOperation.test.ts deleted file mode 100644 index c56713316..000000000 --- a/tests/integration/tests/enhancements/with-policy/currentOperation.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { loadModelWithError, loadSchema } from '@zenstackhq/testtools'; - -describe('currentOperation tests', () => { - it('works with specific rules', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation() == 'create') - } - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation() == 'read') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with all rule', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('all', currentOperation() == 'create') - } - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation() == 'read') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with upper case', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation('upper') == 'CREATE') - } - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation('upper') == 'READ') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with lower case', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation('lower') == 'create') - } - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation('lower') == 'read') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with capitalization', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation('capitalize') == 'Create') - } - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation('capitalize') == 'create') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('works with uncapitalization', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation('uncapitalize') == 'create') - } - model Post { - id Int @id - @@allow('read', true) - @@allow('create', currentOperation('uncapitalize') == 'read') - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); - }); - - it('complains when used outside policies', async () => { - await expect( - loadModelWithError( - ` - model User { - id String @default(currentOperation()) - } - ` - ) - ).resolves.toContain('function "currentOperation" is not allowed in the current context: DefaultValue'); - }); - - it('complains when casing argument is invalid', async () => { - await expect( - loadModelWithError( - ` - model User { - id String @id - @@allow('create', currentOperation('foo') == 'User') - } - ` - ) - ).resolves.toContain('argument must be one of: "original", "upper", "lower", "capitalize", "uncapitalize"'); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts b/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts deleted file mode 100644 index 0a57bb6bb..000000000 --- a/tests/integration/tests/enhancements/with-policy/deep-nested.test.ts +++ /dev/null @@ -1,662 +0,0 @@ -import { loadSchema, type FullDbClientContract } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy:deep nested', () => { - let origDir: string; - - const model = ` - // M1 - M2 - M3 - // -* M4 - model M1 { - myId String @id @default(cuid()) - m2 M2? - value Int @default(0) - - @@allow('all', true) - @@deny('create', m2.m4?[value == 100]) - @@deny('update', m2.m4?[value == 101]) - @@deny('read', value == 100) - } - - model M2 { - id Int @id @default(autoincrement()) - value Int - m1 M1 @relation(fields: [m1Id], references: [myId], onDelete: Cascade) - m1Id String @unique - - m3 M3? - m4 M4[] - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 1) - @@allow('delete', value > 2) - } - - model M3 { - id String @id @default(cuid()) - value Int - m2 M2 @relation(fields: [m2Id], references: [id], onDelete: Cascade) - m2Id Int @unique - - @@allow('read', true) - @@allow('create', value > 10) - @@allow('update', value > 1) - @@allow('delete', value > 2) - @@deny('read', value == 200) - } - - model M4 { - id String @id @default(cuid()) - value Int - m2 M2? @relation(fields: [m2Id], references: [id], onDelete: Cascade) - m2Id Int? - - @@unique([m2Id, value]) - - @@allow('read', true) - @@allow('create', value > 20) - @@allow('update', value > 21) - @@allow('delete', value > 22) - @@deny('read', value == 200) - } - `; - - let db: FullDbClientContract; - let prisma: FullDbClientContract; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - beforeEach(async () => { - const params = await loadSchema(model); - db = params.enhance(); - prisma = params.prisma; - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('read', async () => { - await prisma.m1.create({ - data: { - myId: '1', - m2: { - create: { - value: 1, - m3: { - create: { id: '3-1', value: 31 }, - }, - m4: { - create: [{ value: 41 }, { value: 42 }], - }, - }, - }, - }, - }); - // all readable - let r = await db.m1.findUnique({ - where: { myId: '1' }, - include: { m2: { include: { m3: true, m4: true } } }, - }); - expect(r.m2.m3).toBeTruthy(); - expect(r.m2.m4).toHaveLength(2); - r = await db.m3.findUnique({ where: { id: '3-1' }, include: { m2: { include: { m1: true } } } }); - expect(r.m2.m1).toBeTruthy(); - - await prisma.m1.create({ - data: { - myId: '2', - m2: { - create: { - value: 1, - m3: { - create: { value: 200 }, - }, - m4: { - create: [{ value: 22 }, { value: 200 }], - }, - }, - }, - }, - }); - // check filtered - r = await db.m1.findUnique({ - where: { myId: '2' }, - include: { m2: { include: { m3: true, m4: true } } }, - }); - expect(r.m2.m3).toBeNull(); - expect(r.m2.m4).toHaveLength(1); - - await prisma.m1.create({ - data: { - myId: '3', - value: 100, - m2: { - create: { - value: 1, - m3: { - create: { id: '3-2', value: 31 }, - }, - }, - }, - }, - }); - // check hoisted filtering, due to m1 is not readable - r = await db.m3.findUnique({ where: { id: '3-2' }, include: { m2: { include: { m1: true } } } }); - expect(r).toBeNull(); - }); - - it('create', async () => { - await expect( - db.m1.create({ - data: { - myId: '1', - m2: { - create: { - value: 1, - m3: { - create: { - id: 'm3-1', - value: 11, - }, - }, - m4: { - create: [ - { id: 'm4-1', value: 22 }, - { id: 'm4-2', value: 23 }, - ], - }, - }, - }, - }, - }) - ).toResolveTruthy(); - - const r = await db.m1.create({ - include: { m2: { include: { m3: true, m4: true } } }, - data: { - myId: '2', - m2: { - create: { - value: 2, - m3: { - connect: { - id: 'm3-1', - }, - }, - m4: { - connect: [{ id: 'm4-1' }], - connectOrCreate: [ - { - where: { id: 'm4-2' }, - create: { id: 'm4-new', value: 24 }, - }, - { - where: { id: 'm4-3' }, - create: { id: 'm4-3', value: 25 }, - }, - ], - }, - }, - }, - }, - }); - expect(r.m2.m3.id).toBe('m3-1'); - expect(r.m2.m4[0].id).toBe('m4-1'); - expect(r.m2.m4[1].id).toBe('m4-2'); - expect(r.m2.m4[2].id).toBe('m4-3'); - - // deep create violation - await expect( - db.m1.create({ - data: { - m2: { - create: { - value: 1, - m4: { - create: [{ value: 20 }, { value: 22 }], - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // deep create violation due to deep policy - await expect( - db.m1.create({ - data: { - m2: { - create: { - value: 1, - m4: { - create: { value: 100 }, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // deep connect violation via deep policy: @@deny('create', m2.m4?[value == 100]) - await db.m4.create({ - data: { - id: 'm4-value-100', - value: 100, - }, - }); - await expect( - db.m1.create({ - data: { - m2: { - create: { - value: 1, - m4: { - connect: { id: 'm4-value-100' }, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // create read-back filter: M4 @@deny('read', value == 200) - const r1 = await db.m1.create({ - include: { m2: { include: { m4: true } } }, - data: { - m2: { - create: { - value: 1, - m4: { - create: [{ value: 200 }, { value: 201 }], - }, - }, - }, - }, - }); - expect(r1.m2.m4).toHaveLength(1); - - // create read-back filtering: M3 @@deny('read', value == 200) - const r2 = await db.m1.create({ - include: { m2: { include: { m3: true } } }, - data: { - m2: { - create: { - value: 1, - m3: { - create: { value: 200 }, - }, - }, - }, - }, - }); - expect(r2.m2.m3).toBeNull(); - }); - - it('update simple nested', async () => { - await db.m1.create({ - data: { myId: '1' }, - }); - - // success - await expect( - db.m1.update({ - where: { myId: '1' }, - include: { m2: { include: { m3: true, m4: true } } }, - data: { - m2: { - create: { - value: 2, - m3: { - create: { id: 'm3-1', value: 11 }, - }, - m4: { - create: [ - { id: 'm4-1', value: 22 }, - { id: 'm4-2', value: 23 }, - ], - }, - }, - }, - }, - }) - ).toResolveTruthy(); - - // deep update with connect/disconnect/delete success - await db.m4.create({ - data: { - id: 'm4-3', - value: 24, - }, - }); - const r = await db.m1.update({ - where: { myId: '1' }, - include: { m2: { include: { m4: true } } }, - data: { - m2: { - update: { - m4: { - connect: [{ id: 'm4-3' }], - disconnect: { id: 'm4-1' }, - delete: { id: 'm4-2' }, - }, - }, - }, - }, - }); - expect(r.m2.m4).toHaveLength(1); - expect(r.m2.m4[0].id).toBe('m4-3'); - - // reconnect m14-1, create m14-2 - await expect( - db.m1.update({ - where: { myId: '1' }, - include: { m2: { include: { m4: true } } }, - data: { - m2: { - update: { - m4: { - connect: [{ id: 'm4-1' }], - create: { id: 'm4-2', value: 23 }, - }, - }, - }, - }, - }) - ).toResolveTruthy(); - - // deep update violation - await expect( - db.m1.update({ - where: { myId: '1' }, - data: { - m2: { - update: { - m4: { - create: { value: 20 }, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // deep update violation via deep policy: @@deny('update', m2.m4?[value == 101]) - await db.m1.create({ - data: { - myId: '2', - m2: { - create: { - value: 2, - m4: { - create: { id: 'm4-101', value: 101 }, - }, - }, - }, - }, - }); - await expect( - db.m1.update({ - where: { myId: '2' }, - data: { value: 1 }, - }) - ).toBeRejectedByPolicy(); - - // update read-back filter: M4 @@deny('read', value == 200) - const r1 = await db.m1.update({ - where: { myId: '1' }, - include: { m2: { include: { m4: true } } }, - data: { - m2: { - update: { - m4: { - update: { - where: { id: 'm4-1' }, - data: { value: 200 }, - }, - }, - }, - }, - }, - }); - expect(r1.m2.m4).toHaveLength(2); - expect(r1.m2.m4).not.toContain(expect.objectContaining({ id: 'm4-1' })); - - // update read-back rejection: M3 @@deny('read', value == 200) - const r2 = await db.m1.update({ - where: { myId: '1' }, - include: { m2: { include: { m3: true } } }, - data: { - m2: { - update: { - m3: { - update: { value: 200 }, - }, - }, - }, - }, - }); - expect(r2.m2.m3).toBeNull(); - }); - - it('update createMany/updateMany/deleteMany nested', async () => { - await db.m1.create({ - data: { - myId: '1', - m2: { - create: { - id: 1, - value: 2, - }, - }, - }, - }); - - await db.m1.create({ - data: { - myId: '2', - m2: { - create: { - id: 2, - value: 2, - }, - }, - }, - }); - - // createMany with duplicate - await expect( - db.m1.update({ - where: { myId: '1' }, - data: { - m2: { - update: { - m4: { - createMany: { - data: [ - { id: 'm4-1', value: 21 }, - { id: 'm4-1', value: 22 }, - ], - }, - }, - }, - }, - }, - }) - ).rejects.toThrow('Unique constraint failed'); - - // createMany skip duplicate - await db.m1.update({ - where: { myId: '1' }, - data: { - m2: { - update: { - m4: { - createMany: { - skipDuplicates: true, - data: [ - { id: 'm4-1', value: 21 }, // should be created - { id: 'm4-1', value: 211 }, // should be skipped - { id: 'm4-2', value: 22 }, // should be created - ], - }, - }, - }, - }, - }, - }); - await expect(db.m4.findMany()).resolves.toHaveLength(2); - - // createMany skip duplicate with compound unique involving fk - await db.m1.update({ - where: { myId: '2' }, - data: { - m2: { - update: { - m4: { - createMany: { - skipDuplicates: true, - data: [ - { id: 'm4-3', value: 21 }, // should be created - { id: 'm4-4', value: 21 }, // should be skipped - ], - }, - }, - }, - }, - }, - }); - 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 }])); - - // updateMany, filtered out by policy - await db.m1.update({ - where: { myId: '1' }, - data: { - m2: { - update: { - m4: { - updateMany: { - where: { - id: 'm4-1', - }, - data: { - value: 210, - }, - }, - }, - }, - }, - }, - }); - 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({ - where: { myId: '1' }, - data: { - m2: { - update: { - m4: { - updateMany: { - where: { - id: 'm4-2', - }, - data: { - 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({ - where: { myId: '1' }, - data: { - m2: { - update: { - m4: { - deleteMany: { - id: 'm4-1', - }, - }, - }, - }, - }, - }); - await expect(db.m4.findMany()).resolves.toHaveLength(3); - - // deleteMany, success - await db.m1.update({ - where: { myId: '1' }, - data: { - m2: { - update: { - m4: { - deleteMany: { - id: 'm4-2', - }, - }, - }, - }, - }, - }); - await expect(db.m4.findMany()).resolves.toHaveLength(2); - }); - - it('delete', async () => { - await db.m1.create({ - data: { - myId: '1', - m2: { - create: { - value: 1, - m4: { - create: [{ value: 200 }, { value: 22 }], - }, - }, - }, - }, - }); - - // delete read-back filtered: M4 @@deny('read', value == 200) - const r = await db.m1.delete({ - where: { myId: '1' }, - include: { m2: { select: { m4: true } } }, - }); - expect(r.m2.m4).toHaveLength(1); - - await expect(db.m4.findMany()).resolves.toHaveLength(0); - - await db.m1.create({ - data: { - myId: '2', - m2: { - create: { - value: 1, - m3: { - create: { value: 200 }, - }, - }, - }, - }, - }); - - // delete read-back filtered: M3 @@deny('read', value == 200) - const r1 = await db.m1.delete({ - where: { myId: '2' }, - include: { m2: { select: { m3: { select: { id: true } } } } }, - }); - expect(r1.m2.m3).toBeNull(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/empty-policy.test.ts b/tests/integration/tests/enhancements/with-policy/empty-policy.test.ts deleted file mode 100644 index ee0b61850..000000000 --- a/tests/integration/tests/enhancements/with-policy/empty-policy.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy:empty policy', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('direct operations', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - } - ` - ); - - const db = enhance(); - - await prisma.model.create({ data: { id: '1', value: 0 } }); - await expect(db.model.create({ data: {} })).toBeRejectedByPolicy(); - - 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' } })).toBeNotFound(); - await expect(db.model.findFirstOrThrow({ where: { id: '1' } })).toBeNotFound(); - - await expect(db.model.create({ data: {} })).toBeRejectedByPolicy(); - await expect(db.model.createMany({ data: [{}] })).toBeRejectedByPolicy(); - - await expect(db.model.update({ where: { id: '1' }, data: { value: 1 } })).toBeRejectedByPolicy(); - await expect(db.model.updateMany({ data: { value: 1 } })).toBeRejectedByPolicy(); - await expect( - db.model.upsert({ - where: { id: '1' }, - create: { value: 1 }, - update: { value: 1 }, - }) - ).toBeRejectedByPolicy(); - - await expect(db.model.delete({ where: { id: '1' } })).toBeRejectedByPolicy(); - await expect(db.model.deleteMany()).toBeRejectedByPolicy(); - - 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); - }); - - it('to-many write', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - } - ` - ); - - const db = enhance(); - - await expect( - db.m1.create({ - data: { - m2: { - create: [{}], - }, - }, - }) - ).toBeRejectedByPolicy(); - }); - - it('to-one write', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - } - ` - ); - - const db = enhance(); - - await expect( - db.m1.create({ - data: { - m2: { - create: {}, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/field-comparison.test.ts b/tests/integration/tests/enhancements/with-policy/field-comparison.test.ts deleted file mode 100644 index f130b2c94..000000000 --- a/tests/integration/tests/enhancements/with-policy/field-comparison.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { loadSchema, createPostgresDb, dropPostgresDb } from '@zenstackhq/testtools'; -import path from 'path'; - -const DB_NAME = 'field-comparison'; - -describe('Policy: field comparison tests', () => { - let origDir: string; - let dbUrl: string; - let prisma: any; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - beforeEach(async () => { - dbUrl = await createPostgresDb(DB_NAME); - }); - - afterEach(async () => { - if (prisma) { - await prisma.$disconnect(); - prisma = undefined; - } - await dropPostgresDb(DB_NAME); - process.chdir(origDir); - }); - - it('field comparison success with input check', async () => { - const r = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - x Int - y Int - - @@allow('create', x > y) - @@allow('read', true) - } - `, - { provider: 'postgresql', dbUrl } - ); - - prisma = r.prisma; - const db = r.enhance(); - await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 2, y: 1 } })).toResolveTruthy(); - }); - - it('field comparison success with policy check', async () => { - const r = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - x Int @default(0) - y Int @default(0) - - @@allow('create', x > y) - @@allow('read', true) - } - `, - { provider: 'postgresql', dbUrl } - ); - - prisma = r.prisma; - const db = r.enhance(); - await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 2, y: 1 } })).toResolveTruthy(); - }); - - it('field in operator success with input check', async () => { - const r = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - x String - y String[] - - @@allow('create', x in y) - @@allow('read', x in y) - } - `, - { provider: 'postgresql', dbUrl } - ); - - prisma = r.prisma; - const db = r.enhance(); - await expect(db.model.create({ data: { x: 'a', y: ['b', 'c'] } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 'a', y: ['a', 'c'] } })).toResolveTruthy(); - }); - - it('field in operator success with policy check', async () => { - const r = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - x String @default('x') - y String[] - - @@allow('create', x in y) - @@allow('read', x in y) - } - `, - { provider: 'postgresql', dbUrl } - ); - - prisma = r.prisma; - const db = r.enhance(); - await expect(db.model.create({ data: { x: 'a', y: ['b', 'c'] } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 'a', y: ['a', 'c'] } })).toResolveTruthy(); - }); - - it('field comparison type error', async () => { - await expect( - loadSchema( - ` - model Model { - id String @id @default(uuid()) - x Int - y String - - @@allow('create', x > y) - @@allow('read', true) - } - ` - ) - ).rejects.toThrow(/invalid operand type/); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts b/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts deleted file mode 100644 index de103f4a5..000000000 --- a/tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts +++ /dev/null @@ -1,1394 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('Policy: field-level policy', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('read simple', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - admin Boolean @default(false) - models Model[] - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('read', x > 0) - z Int @deny('read', x <= 0) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, admin: true } }); - - const db = enhance(); - let r; - - // y and x are unreadable - - r = await db.model.create({ - data: { id: 1, x: 0, y: 0, z: 0, ownerId: 1 }, - }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - expect(r.z).toBeUndefined(); - - r = await db.model.findUnique({ where: { id: 1 } }); - expect(r.y).toBeUndefined(); - expect(r.z).toBeUndefined(); - - r = await db.user.findUnique({ where: { id: 1 }, select: { models: true } }); - expect(r.models[0].y).toBeUndefined(); - expect(r.models[0].z).toBeUndefined(); - - r = await db.user.findUnique({ where: { id: 1 }, select: { models: { select: { y: true } } } }); - expect(r.models[0].y).toBeUndefined(); - expect(r.models[0].z).toBeUndefined(); - - r = await db.user.findUnique({ where: { id: 1 } }).models(); - expect(r[0].y).toBeUndefined(); - expect(r[0].z).toBeUndefined(); - - r = await db.user.findUnique({ where: { id: 1 } }).models({ select: { y: true } }); - expect(r[0].y).toBeUndefined(); - expect(r[0].z).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - expect(r.z).toBeUndefined(); - - r = await db.model.findUnique({ select: { y: true }, where: { id: 1 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toBeUndefined(); - expect(r.z).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: false, y: true }, where: { id: 1 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toBeUndefined(); - expect(r.z).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: true, y: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - expect(r.z).toBeUndefined(); - - r = await db.model.findUnique({ include: { owner: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.owner).toBeTruthy(); - expect(r.y).toBeUndefined(); - expect(r.z).toBeUndefined(); - - // y is readable - - r = await db.model.create({ - data: { id: 2, x: 1, y: 0, z: 0, ownerId: 1 }, - }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0, z: 0 })); - - r = await db.model.findUnique({ where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0, z: 0 })); - - r = await db.user.findUnique({ where: { id: 1 }, select: { models: { where: { id: 2 } } } }); - expect(r.models[0]).toEqual(expect.objectContaining({ x: 1, y: 0, z: 0 })); - - r = await db.user.findUnique({ - where: { id: 1 }, - select: { models: { where: { id: 2 }, select: { y: true, z: true } } }, - }); - expect(r.models[0]).toEqual(expect.objectContaining({ y: 0, z: 0 })); - - r = await db.user.findUnique({ where: { id: 1 } }).models({ where: { id: 2 } }); - expect(r[0]).toEqual(expect.objectContaining({ x: 1, y: 0, z: 0 })); - - r = await db.user.findUnique({ where: { id: 1 } }).models({ where: { id: 2 }, select: { y: true } }); - expect(r[0]).toEqual(expect.objectContaining({ y: 0 })); - - r = await db.model.findUnique({ select: { x: true }, where: { id: 2 } }); - expect(r.x).toEqual(1); - expect(r.y).toBeUndefined(); - expect(r.z).toBeUndefined(); - - r = await db.model.findUnique({ select: { y: true }, where: { id: 2 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toEqual(0); - expect(r.z).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: false, y: true, z: true }, where: { id: 2 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toEqual(0); - expect(r.z).toEqual(0); - - r = await db.model.findUnique({ select: { x: true, y: true, z: true }, where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0, z: 0 })); - - r = await db.model.findUnique({ include: { owner: true }, where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0, z: 0 })); - expect(r.owner).toBeTruthy(); - }); - - it('read override', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - admin Boolean @default(false) - models Model[] - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('read', x > 0, true) - owner User @relation(fields: [ownerId], references: [id]) @allow('read', x > 1, true) - ownerId Int - - @@allow('create', true) - @@allow('read', x > 1) - } - ` - ); - - await prisma.user.create({ data: { id: 1, admin: true } }); - - const db = enhance(); - - // created but can't read back - await expect( - db.model.create({ - data: { id: 1, x: 0, y: 0, ownerId: 1 }, - }) - ).toBeRejectedByPolicy(); - await expect(prisma.model.findUnique({ where: { id: 1 } })).resolves.toBeTruthy(); - await expect(db.model.findUnique({ where: { id: 1 } })).resolves.toBeNull(); - - // y is readable through override - // created but can't read back - await expect( - db.model.create({ - data: { id: 2, x: 1, y: 0, ownerId: 1 }, - }) - ).toBeRejectedByPolicy(); - - // y can be read back - await expect( - db.model.create({ - data: { id: 3, x: 1, y: 0, ownerId: 1 }, - select: { y: true }, - }) - ).resolves.toEqual({ y: 0 }); - - await expect(db.model.findUnique({ where: { id: 3 } })).resolves.toBeNull(); - await expect(db.model.findUnique({ where: { id: 3 }, select: { y: true } })).resolves.toEqual({ y: 0 }); - await expect(db.model.findUnique({ where: { id: 3 }, select: { x: true, y: true } })).resolves.toBeNull(); - await expect(db.model.findUnique({ where: { id: 3 }, select: { owner: true, y: true } })).resolves.toBeNull(); - await expect(db.model.findUnique({ where: { id: 3 }, include: { owner: true } })).resolves.toBeNull(); - - // y and owner are readable through override - await expect( - db.model.create({ - data: { id: 4, x: 2, y: 0, ownerId: 1 }, - select: { y: true }, - }) - ).resolves.toEqual({ y: 0 }); - await expect( - db.model.findUnique({ where: { id: 4 }, select: { owner: true, y: true } }) - ).resolves.toMatchObject({ - owner: expect.objectContaining({ admin: true }), - y: 0, - }); - await expect(db.model.findUnique({ where: { id: 4 }, include: { owner: true } })).resolves.toMatchObject({ - owner: expect.objectContaining({ admin: true }), - y: 0, - }); - }); - - it('read filter with auth', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - admin Boolean @default(false) - models Model[] - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('read', auth().admin) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, admin: true } }); - - let db = enhance({ id: 1, admin: false }); - let r; - - // y is unreadable - - r = await db.model.create({ - data: { - id: 1, - x: 0, - y: 0, - ownerId: 1, - }, - }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ where: { id: 1 } }); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { y: true }, where: { id: 1 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: false, y: true }, where: { id: 1 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: true, y: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ include: { owner: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.owner).toBeTruthy(); - expect(r.y).toBeUndefined(); - - // y is readable - db = enhance({ id: 1, admin: true }); - r = await db.model.create({ - data: { - id: 2, - x: 1, - y: 0, - ownerId: 1, - }, - }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); - - r = await db.model.findUnique({ where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); - - r = await db.model.findUnique({ select: { x: true }, where: { id: 2 } }); - expect(r.x).toEqual(1); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { y: true }, where: { id: 2 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toEqual(0); - - r = await db.model.findUnique({ select: { x: false, y: true }, where: { id: 2 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toEqual(0); - - r = await db.model.findUnique({ select: { x: true, y: true }, where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); - - r = await db.model.findUnique({ include: { owner: true }, where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); - expect(r.owner).toBeTruthy(); - }); - - it('read filter with relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - admin Boolean @default(false) - models Model[] - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('read', owner.admin) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, admin: false } }); - await prisma.user.create({ data: { id: 2, admin: true } }); - - const db = enhance(); - let r; - - // y is unreadable - - r = await db.model.create({ - data: { - id: 1, - x: 0, - y: 0, - ownerId: 1, - }, - }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ where: { id: 1 } }); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { y: true }, where: { id: 1 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: false, y: true }, where: { id: 1 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { x: true, y: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ include: { owner: true }, where: { id: 1 } }); - expect(r.x).toEqual(0); - expect(r.owner).toBeTruthy(); - expect(r.y).toBeUndefined(); - - // y is readable - r = await db.model.create({ - data: { - id: 2, - x: 1, - y: 0, - ownerId: 2, - }, - }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); - - r = await db.model.findUnique({ where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); - - r = await db.model.findUnique({ select: { x: true }, where: { id: 2 } }); - expect(r.x).toEqual(1); - expect(r.y).toBeUndefined(); - - r = await db.model.findUnique({ select: { y: true }, where: { id: 2 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toEqual(0); - - r = await db.model.findUnique({ select: { x: false, y: true }, where: { id: 2 } }); - expect(r.x).toBeUndefined(); - expect(r.y).toEqual(0); - - r = await db.model.findUnique({ select: { x: true, y: true }, where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); - - r = await db.model.findUnique({ include: { owner: true }, where: { id: 2 } }); - expect(r).toEqual(expect.objectContaining({ x: 1, y: 0 })); - expect(r.owner).toBeTruthy(); - }); - - it('read coverage', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('read', x > 0) - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - let r; - - // y is unreadable - r = await db.model.create({ - data: { - id: 1, - x: 0, - y: 0, - }, - }); - - r = await db.model.findUnique({ where: { id: 1 } }); - expect(r.y).toBeUndefined(); - - r = await db.model.findUniqueOrThrow({ where: { id: 1 } }); - expect(r.y).toBeUndefined(); - - r = await db.model.findFirst({ where: { id: 1 } }); - expect(r.y).toBeUndefined(); - - r = await db.model.findFirstOrThrow({ where: { id: 1 } }); - expect(r.y).toBeUndefined(); - - await db.model.create({ - data: { - id: 2, - x: 1, - y: 0, - }, - }); - r = await db.model.findMany({ where: { x: { gte: 0 } } }); - expect(r[0].y).toBeUndefined(); - expect(r[1].y).toEqual(0); - }); - - it('read relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - admin Boolean @default(false) - posts Post[] @allow('read', admin) - - @@allow('all', true) - } - - model Post { - id Int @id @default(autoincrement()) - author User? @relation(fields: [authorId], references: [id]) @allow('read', author.admin) - authorId Int @allow('read', author.admin) - title String - published Boolean @default(false) - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - admin: false, - posts: { - create: [{ id: 1, title: 'post1' }], - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - admin: true, - posts: { - create: [{ id: 2, title: 'post2' }], - }, - }, - }); - - const db = enhance(); - - // read to-many relation - let r = await db.user.findUnique({ - where: { id: 1 }, - include: { posts: true }, - }); - expect(r.posts).toBeUndefined(); - r = await db.user.findUnique({ - where: { id: 2 }, - include: { posts: true }, - }); - expect(r.posts).toHaveLength(1); - - // read to-one relation - r = await db.post.findUnique({ where: { id: 1 }, include: { author: true } }); - expect(r.author).toBeUndefined(); - expect(r.authorId).toBeUndefined(); - r = await db.post.findUnique({ where: { id: 1 }, select: { author: { select: { admin: true } } } }); - expect(r.author).toBeUndefined(); - r = await db.post.findUnique({ where: { id: 2 }, include: { author: true } }); - expect(r.author).toBeTruthy(); - expect(r.authorId).toBeTruthy(); - }); - - it('update simple', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - models Model[] - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('update', x > 0) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - @@allow('create,read', true) - @@allow('update', y > 0) - } - ` - ); - - await prisma.user.create({ - data: { id: 1 }, - }); - const db = enhance(); - - await db.model.create({ - data: { id: 1, x: 0, y: 0, ownerId: 1 }, - }); - await expect( - db.model.update({ - where: { id: 1 }, - data: { y: 2 }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.model.update({ - where: { id: 1 }, - data: { x: 2 }, - }) - ).toBeRejectedByPolicy(); - - await db.model.create({ - data: { id: 2, x: 0, y: 1, ownerId: 1 }, - }); - await expect( - db.model.update({ - where: { id: 2 }, - data: { y: 2 }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.model.update({ - where: { id: 2 }, - data: { x: 2 }, - }) - ).toResolveTruthy(); - - await db.model.create({ - data: { id: 3, x: 1, y: 1, ownerId: 1 }, - }); - await expect( - db.model.update({ - where: { id: 3 }, - data: { y: 2 }, - }) - ).toResolveTruthy(); - }); - - it('update with override', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('update', x > 0, true) @deny('update', x == 100) - z Int @allow('update', x > 1, true) - - @@allow('create,read', true) - @@allow('update', y > 0) - } - ` - ); - - const db = enhance(); - - await db.model.create({ - data: { id: 1, x: 0, y: 0, z: 0 }, - }); - - await expect( - db.model.update({ - where: { id: 1 }, - data: { y: 2 }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.model.update({ - where: { id: 1 }, - data: { x: 2 }, - }) - ).toBeRejectedByPolicy(); - - await db.model.create({ - data: { id: 2, x: 1, y: 0, z: 0 }, - }); - await expect( - db.model.update({ - where: { id: 2 }, - data: { x: 2, y: 1 }, - }) - ).toBeRejectedByPolicy(); // denied because field `x` doesn't have override - await expect( - db.model.update({ - where: { id: 2 }, - data: { y: 1, z: 1 }, - }) - ).toBeRejectedByPolicy(); // denied because field `z` override not satisfied - await expect( - db.model.update({ - where: { id: 2 }, - data: { y: 1 }, - }) - ).toResolveTruthy(); // allowed by override - await expect(db.model.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ y: 1 }); - - await db.model.create({ - data: { id: 3, x: 2, y: 0, z: 0 }, - }); - await expect( - db.model.update({ - where: { id: 3 }, - data: { y: 1, z: 1 }, - }) - ).toResolveTruthy(); // allowed by override - await expect(db.model.findUnique({ where: { id: 3 } })).resolves.toMatchObject({ y: 1, z: 1 }); - - await db.model.create({ - data: { id: 4, x: 100, y: 0, z: 0 }, - }); - await expect( - db.model.update({ - where: { id: 4 }, - data: { y: 1 }, - }) - ).toBeRejectedByPolicy(); // can't be allowed by override due to field-level deny - }); - - it('update filter with relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - models Model[] - admin Boolean @default(false) - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('update', owner.admin) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { id: 1, admin: false }, - }); - await prisma.user.create({ - data: { id: 2, admin: true }, - }); - const db = enhance(); - - await db.model.create({ - data: { id: 1, x: 0, y: 0, ownerId: 1 }, - }); - await expect( - db.model.update({ - where: { id: 1 }, - data: { y: 2 }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.model.update({ - where: { id: 1 }, - data: { x: 2 }, - }) - ).toResolveTruthy(); - - await db.model.create({ - data: { id: 2, x: 0, y: 0, ownerId: 2 }, - }); - await expect( - db.model.update({ - where: { id: 2 }, - data: { y: 2 }, - }) - ).toResolveTruthy(); - }); - - it('update with nested to-many relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - models Model[] - admin Boolean @default(false) - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('update', owner.admin) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { id: 1, admin: false, models: { create: { id: 1, x: 0, y: 0 } } }, - }); - await prisma.user.create({ - data: { id: 2, admin: true, models: { create: { id: 2, x: 0, y: 0 } } }, - }); - const db = enhance(); - - await expect( - db.user.update({ - where: { id: 1 }, - data: { models: { update: { where: { id: 1 }, data: { y: 2 } } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { models: { update: { where: { id: 1 }, data: { x: 2 } } } }, - }) - ).toResolveTruthy(); - - await expect( - db.user.update({ - where: { id: 2 }, - data: { models: { update: { where: { id: 2 }, data: { y: 2 } } } }, - }) - ).toResolveTruthy(); - }); - - it('update with nested to-one relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - model Model? - admin Boolean @default(false) - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('update', owner.admin) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int @unique - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { id: 1, admin: false, model: { create: { id: 1, x: 0, y: 0 } } }, - }); - await prisma.user.create({ - data: { id: 2, admin: true, model: { create: { id: 2, x: 0, y: 0 } } }, - }); - const db = enhance(); - - await expect( - db.user.update({ - where: { id: 1 }, - data: { model: { update: { data: { y: 2 } } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { model: { update: { y: 2 } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { model: { update: { data: { x: 2 } } } }, - }) - ).toResolveTruthy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { model: { update: { x: 2 } } }, - }) - ).toResolveTruthy(); - - await expect( - db.user.update({ - where: { id: 2 }, - data: { model: { update: { data: { y: 2 } } } }, - }) - ).toResolveTruthy(); - await expect( - db.user.update({ - where: { id: 2 }, - data: { model: { update: { y: 2 } } }, - }) - ).toResolveTruthy(); - }); - - it('update with connect to-many relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - models Model[] - admin Boolean @default(false) - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - value Int - owner User? @relation(fields: [ownerId], references: [id]) - ownerId Int? @allow('update', value > 0) - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, admin: false } }); - await prisma.user.create({ data: { id: 2, admin: true } }); - await prisma.model.create({ data: { id: 1, value: 0 } }); - await prisma.model.create({ data: { id: 2, value: 1 } }); - - const db = enhance(); - - await expect( - db.model.update({ - where: { id: 1 }, - data: { owner: { connect: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.model.update({ - where: { id: 1 }, - data: { owner: { disconnect: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.model.update({ - where: { id: 2 }, - data: { owner: { connect: { id: 1 } } }, - }) - ).toResolveTruthy(); - await expect( - db.model.update({ - where: { id: 2 }, - data: { owner: { disconnect: { id: 1 } } }, - }) - ).toResolveTruthy(); - - await expect( - db.user.update({ - where: { id: 1 }, - data: { models: { connect: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ - where: { id: 1 }, - data: { models: { connect: { id: 1 } } }, - }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { models: { disconnect: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { models: { set: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.update({ - where: { id: 1 }, - data: { models: { connect: { id: 2 } } }, - }) - ).toResolveTruthy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { models: { disconnect: { id: 2 } } }, - }) - ).toResolveTruthy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { models: { set: { id: 2 } } }, - }) - ).toResolveTruthy(); - }); - - it('update with connect to-one relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - model Model? - admin Boolean @default(false) - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - value Int - owner User? @relation(fields: [ownerId], references: [id]) - ownerId Int? @unique @allow('update', value > 0) - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, admin: false } }); - await prisma.user.create({ data: { id: 2, admin: true } }); - await prisma.model.create({ data: { id: 1, value: 0 } }); - await prisma.model.create({ data: { id: 2, value: 1 } }); - - const db = enhance(); - - await expect( - db.model.update({ - where: { id: 1 }, - data: { owner: { connect: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.model.update({ - where: { id: 1 }, - data: { owner: { disconnect: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.model.update({ - where: { id: 1 }, - data: { owner: { set: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.model.update({ - where: { id: 2 }, - data: { owner: { connect: { id: 1 } } }, - }) - ).toResolveTruthy(); - await expect( - db.model.update({ - where: { id: 2 }, - data: { owner: { disconnect: { id: 1 } } }, - }) - ).toResolveTruthy(); - - await expect( - db.user.update({ - where: { id: 1 }, - data: { model: { connect: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - await prisma.user.update({ - where: { id: 1 }, - data: { model: { connect: { id: 1 } } }, - }); - await expect( - db.user.update({ - where: { id: 1 }, - data: { model: { disconnect: { id: 1 } } }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.update({ - where: { id: 1 }, - data: { model: { connect: { id: 2 } } }, - }) - ).toResolveTruthy(); - await expect( - db.user.update({ - where: { id: 1 }, - data: { model: { disconnect: { id: 2 } } }, - }) - ).toResolveTruthy(); - }); - - it('updateMany simple', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - models Model[] - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('update', x > 0) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - models: { - create: [ - { id: 1, x: 0, y: 0 }, - { id: 2, x: 1, y: 0 }, - ], - }, - }, - }); - const db = enhance(); - - await expect(db.model.updateMany({ data: { y: 2 } })).resolves.toEqual({ count: 1 }); - await expect(db.model.findUnique({ where: { id: 1 } })).resolves.toEqual( - expect.objectContaining({ x: 0, y: 0 }) - ); - await expect(db.model.findUnique({ where: { id: 2 } })).resolves.toEqual( - expect.objectContaining({ x: 1, y: 2 }) - ); - }); - - it('updateMany override', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('update', x > 0, override: true) - - @@allow('create,read', true) - @@allow('update', x > 1) - } - ` - ); - - const db = enhance(); - - await db.model.create({ data: { id: 1, x: 0, y: 0 } }); - await db.model.create({ data: { id: 2, x: 1, y: 0 } }); - - await expect(db.model.updateMany({ data: { y: 2 } })).resolves.toEqual({ count: 1 }); - await expect(db.model.findUnique({ where: { id: 1 } })).resolves.toEqual( - expect.objectContaining({ x: 0, y: 0 }) - ); - await expect(db.model.findUnique({ where: { id: 2 } })).resolves.toEqual( - expect.objectContaining({ x: 1, y: 2 }) - ); - - await expect(db.model.updateMany({ data: { x: 2, y: 3 } })).resolves.toEqual({ count: 0 }); - }); - - it('updateMany nested', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - models Model[] - - @@allow('all', true) - } - - model Model { - id Int @id @default(autoincrement()) - x Int - y Int @allow('update', x > 0) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - models: { - create: [ - { id: 1, x: 0, y: 0 }, - { id: 2, x: 1, y: 0 }, - ], - }, - }, - }); - const db = enhance(); - - await expect( - db.user.update({ where: { id: 1 }, data: { models: { updateMany: { data: { y: 2 } } } } }) - ).toResolveTruthy(); - await expect(db.model.findUnique({ where: { id: 1 } })).resolves.toEqual( - expect.objectContaining({ x: 0, y: 0 }) - ); - await expect(db.model.findUnique({ where: { id: 2 } })).resolves.toEqual( - expect.objectContaining({ x: 1, y: 2 }) - ); - - await expect( - db.user.update({ where: { id: 1 }, data: { models: { updateMany: { where: { id: 1 }, data: { y: 2 } } } } }) - ).toResolveTruthy(); - await expect(db.model.findUnique({ where: { id: 1 } })).resolves.toEqual( - expect.objectContaining({ x: 0, y: 0 }) - ); - - await expect( - db.user.update({ where: { id: 1 }, data: { models: { updateMany: { where: { id: 2 }, data: { y: 3 } } } } }) - ).toResolveTruthy(); - await expect(db.model.findUnique({ where: { id: 2 } })).resolves.toEqual( - expect.objectContaining({ x: 1, y: 3 }) - ); - }); - - it('this expression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - username String @allow("all", auth() == this) - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, username: 'test' } }); - - // admin - let r = await enhance({ id: 1, admin: true }).user.findFirst(); - expect(r.username).toEqual('test'); - - // owner - r = await enhance({ id: 1 }).user.findFirst(); - expect(r.username).toEqual('test'); - - // anonymous - r = await enhance().user.findFirst(); - expect(r.username).toBeUndefined(); - - // non-owner - r = await enhance({ id: 2 }).user.findFirst(); - expect(r.username).toBeUndefined(); - }); - - it('collection predicate', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - foos Foo[] - a Int @allow('read', foos?[x > 0 && bars![y > 0]]) - b Int @allow('read', foos^[x == 1]) - - @@allow('all', true) - } - - model Foo { - id Int @id @default(autoincrement()) - x Int - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - bars Bar[] - - @@allow('all', true) - } - - model Bar { - id Int @id @default(autoincrement()) - y Int - foo Foo @relation(fields: [fooId], references: [id]) - fooId Int - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await prisma.user.create({ - data: { - id: 1, - a: 1, - b: 2, - foos: { - create: [ - { x: 0, bars: { create: [{ y: 1 }] } }, - { x: 1, bars: { create: [{ y: 0 }, { y: 1 }] } }, - ], - }, - }, - }); - - let r = await db.user.findUnique({ where: { id: 1 } }); - expect(r.a).toBeUndefined(); - expect(r.b).toBeUndefined(); - - await prisma.user.create({ - data: { - id: 2, - a: 1, - b: 2, - foos: { - create: [{ x: 2, bars: { create: [{ y: 0 }, { y: 1 }] } }], - }, - }, - }); - r = await db.user.findUnique({ where: { id: 2 } }); - expect(r.a).toBeUndefined(); - expect(r.b).toBe(2); - - await prisma.user.create({ - data: { - id: 3, - a: 1, - b: 2, - foos: { - create: [{ x: 2 }], - }, - }, - }); - r = await db.user.findUnique({ where: { id: 3 } }); - expect(r.a).toBe(1); - - await prisma.user.create({ - data: { - id: 4, - a: 1, - b: 2, - foos: { - create: [{ x: 2, bars: { create: [{ y: 1 }, { y: 2 }] } }], - }, - }, - }); - r = await db.user.findUnique({ where: { id: 4 } }); - expect(r.a).toBe(1); - expect(r.b).toBe(2); - }); - - it('deny only without field access', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - role String @deny('update', auth().role != 'ADMIN') - - @@allow('all', true) - } - ` - ); - - const user = await prisma.user.create({ - data: { role: 'USER' }, - }); - - await expect( - enhance({ id: 1, role: 'ADMIN' }).user.update({ - where: { id: user.id }, - data: { role: 'ADMIN' }, - }) - ).toResolveTruthy(); - - await expect( - enhance({ id: 1, role: 'USER' }).user.update({ - where: { id: user.id }, - data: { role: 'ADMIN' }, - }) - ).toBeRejectedByPolicy(); - }); - - it('deny only with field access', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - locked Boolean @default(false) - role String @deny('update', auth().role != 'ADMIN' || locked) - - @@allow('all', true) - } - ` - ); - - const user1 = await prisma.user.create({ - data: { role: 'USER' }, - }); - - await expect( - enhance({ id: 1, role: 'ADMIN' }).user.update({ - where: { id: user1.id }, - data: { role: 'ADMIN' }, - }) - ).toResolveTruthy(); - - await expect( - enhance({ id: 1, role: 'USER' }).user.update({ - where: { id: user1.id }, - data: { role: 'ADMIN' }, - }) - ).toBeRejectedByPolicy(); - - const user2 = await prisma.user.create({ - data: { role: 'USER', locked: true }, - }); - - await expect( - enhance({ id: 1, role: 'ADMIN' }).user.update({ - where: { id: user2.id }, - data: { role: 'ADMIN' }, - }) - ).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts b/tests/integration/tests/enhancements/with-policy/field-validation.test.ts deleted file mode 100644 index d8828b199..000000000 --- a/tests/integration/tests/enhancements/with-policy/field-validation.test.ts +++ /dev/null @@ -1,995 +0,0 @@ -import { CrudFailureReason, isPrismaClientKnownRequestError } from '@zenstackhq/runtime'; -import { FullDbClientContract, createPostgresDb, dropPostgresDb, loadSchema, run } from '@zenstackhq/testtools'; - -describe('Field validation', () => { - let db: FullDbClientContract; - - beforeAll(async () => { - const { enhance, prisma: _prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - password String @password @length(8, 16) - email String? @email @endsWith("@myorg.com") - profileImage String? @url - handle String? @regex("^[0-9a-zA-Z]{4,16}$") - age Int @default(18) @gt(0) @lt(100) - - userData UserData? - tasks Task[] - - @@allow('all', true) - } - - model UserData { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String @unique - - a Int @gt(0) - b Int @gte(0) - c Int @lt(0) - d Int @lte(0) - text1 String @startsWith('abc') - text2 String @endsWith('def') - text3 String @length(min: 3) - text4 String @length(max: 5) - text5 String? @endsWith('xyz') - text6 String? @trim @lower - text7 String? @upper - } - - model Task { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String - slug String @regex("^[0-9a-zA-Z]{4,16}$") @lower - } -`, - { enhancements: ['validation'] } - ); - db = enhance(); - }); - - beforeEach(() => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - }); - - it('direct write simple', async () => { - await expect( - db.user.create({ - data: { - id: '1', - password: 'abc123', - handle: 'hello world', - }, - }) - ).toBeRejectedByPolicy(['8 character', 'handle']); - - let err: any; - try { - await db.user.create({ - data: { - id: '1', - password: 'abc123', - handle: 'hello world', - }, - }); - } catch (_err) { - err = _err; - } - - expect(isPrismaClientKnownRequestError(err)).toBeTruthy(); - expect(err).toMatchObject({ - code: 'P2004', - meta: { - reason: CrudFailureReason.DATA_VALIDATION_VIOLATION, - }, - }); - expect(err.meta.zodErrors).toBeTruthy(); - - await expect( - db.user.create({ - data: { - id: '1', - password: 'abc123!@#', - email: 'something', - handle: 'user1user1user1user1user1', - }, - }) - ).toBeRejectedByPolicy(['email', 'handle']); - - await expect( - db.user.create({ - data: { - id: '1', - password: 'abc123!@#', - email: 'who@myorg.com', - handle: 'user1', - }, - }) - ).toResolveTruthy(); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - password: 'abc123', - email: 'something', - }, - }) - ).toBeRejectedByPolicy(['8 character', 'email']); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - age: { increment: 100 }, - }, - }) - ).toBeRejectedByPolicy(['100']); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - age: { increment: 10 }, - }, - }) - ).toResolveTruthy(); - }); - - it('direct write more', async () => { - await db.user.create({ - data: { - id: '1', - password: 'abc123!@#', - email: 'who@myorg.com', - handle: 'user1', - }, - }); - - await expect( - db.userData.create({ - data: { - userId: '1', - a: 0, - b: -1, - c: 0, - d: 1, - text1: 'a', - text2: 'xyz', - text3: 'a', - text4: 'abcabc', - text5: 'abc', - }, - }) - ).toBeRejectedByPolicy([ - '0 at "a"', - '0 at "b"', - '0 at "c"', - '0 at "d"', - 'text1', - 'text2', - 'text3', - 'text4', - 'text5', - ]); - - await expect( - db.userData.create({ - data: { - userId: '1', - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - }, - }) - ).toResolveTruthy(); - }); - - it('nested create test', async () => { - const user = { - password: 'abc123!@#', - email: 'who@myorg.com', - handle: 'user1', - }; - - const userData = { - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - }; - - const tasks = [ - { - slug: 'abcabc', - }, - { - slug: 'abcdef', - }, - ]; - - await expect( - db.user.create({ - data: { - ...user, - userData: { - create: { - ...userData, - a: 0, - }, - }, - }, - }) - ).toBeRejectedByPolicy(['0 at "a"']); - - await expect( - db.user.create({ - data: { - ...user, - tasks: { - create: { - slug: 'abc', - }, - }, - }, - }) - ).toBeRejectedByPolicy(['slug']); - - await expect( - db.user.create({ - data: { - ...user, - userData: { - connectOrCreate: { - where: { - id: '1', - }, - create: { - ...userData, - a: 0, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(['0 at "a"']); - - await expect( - db.user.create({ - data: { - ...user, - tasks: { - create: [ - { - slug: 'abc', - }, - { - slug: 'abcdef', - }, - ], - }, - }, - }) - ).toBeRejectedByPolicy(['slug']); - - await expect( - db.user.create({ - data: { - ...user, - userData: { - connectOrCreate: { - where: { - id: '1', - }, - create: userData, - }, - }, - tasks: { - create: tasks, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('nested update test', async () => { - const user = { - password: 'abc123!@#', - email: 'who@myorg.com', - handle: 'user1', - }; - - const userData = { - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - }; - - await expect( - db.user.create({ - data: { - id: '1', - ...user, - }, - }) - ).toResolveTruthy(); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - userData: { - create: { - ...userData, - a: 0, - }, - }, - }, - }) - ).toBeRejectedByPolicy(['0 at "a"']); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - tasks: { - create: { - slug: 'abc', - }, - }, - }, - }) - ).toBeRejectedByPolicy(['slug']); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - userData: { - create: { id: '1', ...userData }, - }, - tasks: { - create: { - id: '1', - slug: 'abcabc', - }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - userData: { - update: { - a: 0, - }, - }, - }, - }) - ).toBeRejectedByPolicy(['0 at "a"']); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - tasks: { - update: { - where: { id: '1' }, - data: { - slug: 'abc', - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(['slug']); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - userData: { - update: { - a: 2, - }, - }, - tasks: { - update: { - where: { id: '1' }, - data: { - slug: 'defdef', - }, - }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - userData: { - upsert: { - create: { - ...userData, - a: 0, - }, - update: { - a: 0, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(['0 at "a"']); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - tasks: { - updateMany: { - where: { id: '1' }, - data: { - slug: 'abc', - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(['slug']); - - await expect( - db.user.update({ - where: { id: '1' }, - data: { - userData: { - upsert: { - create: { - ...userData, - }, - update: { - a: 1, - }, - }, - }, - tasks: { - updateMany: { - where: { id: '1' }, - data: { - slug: 'xxxyyy', - }, - }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('string transformation', async () => { - await db.user.create({ - data: { - id: '1', - password: 'abc123!@#', - email: 'who@myorg.com', - handle: 'user1', - }, - }); - - let ud = await db.userData.create({ - data: { - userId: '1', - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - text6: ' AbC ', - text7: 'abc', - }, - }); - expect(ud).toMatchObject({ text6: 'abc', text7: 'ABC' }); - - ud = await db.userData.update({ - where: { id: ud.id }, - data: { - text4: 'xyz', - text6: ' bCD ', - text7: 'bcd', - }, - }); - expect(ud).toMatchObject({ text4: 'xyz', text6: 'bcd', text7: 'BCD' }); - - let u = await db.user.create({ - data: { - id: '2', - password: 'abc123!@#', - email: 'who@myorg.com', - handle: 'user2', - userData: { - create: { - a: 1, - b: 0, - c: -1, - d: 0, - text1: 'abc123', - text2: 'def', - text3: 'aaa', - text4: 'abcab', - text6: ' AbC ', - text7: 'abc', - }, - }, - }, - include: { userData: true }, - }); - expect(u.userData).toMatchObject({ - text6: 'abc', - text7: 'ABC', - }); - - u = await db.user.update({ - where: { id: u.id }, - data: { - userData: { - update: { - data: { text4: 'xyz', text6: ' bCD ', text7: 'bcd' }, - }, - }, - }, - include: { userData: true }, - }); - expect(u.userData).toMatchObject({ text4: 'xyz', text6: 'bcd', text7: 'BCD' }); - - // upsert create - u = await db.user.update({ - where: { id: u.id }, - data: { - tasks: { - upsert: { - where: { id: 'unknown' }, - create: { slug: 'SLUG1' }, - update: {}, - }, - }, - }, - include: { tasks: true }, - }); - expect(u.tasks[0]).toMatchObject({ slug: 'slug1' }); - - // upsert update - u = await db.user.update({ - where: { id: u.id }, - data: { - tasks: { - upsert: { - where: { id: u.tasks[0].id }, - create: {}, - update: { slug: 'SLUG2' }, - }, - }, - }, - include: { tasks: true }, - }); - expect(u.tasks[0]).toMatchObject({ slug: 'slug2' }); - }); -}); - -describe('Model-level validation', () => { - it('create', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int - - @@validate(x > 0) - @@validate(x >= y) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { x: 0, y: 0 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 2, y: 1 } })).toResolveTruthy(); - }); - - it('update', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int - - @@validate(x >= y) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { id: 1, x: 2, y: 1 } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { y: 3 } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: 1 }, data: { x: 3 } })).toResolveTruthy(); - }); - - it('int optionality', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int? - - @@validate(x > 0) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { x: 0 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 1 } })).toResolveTruthy(); - await expect(db.model.create({ data: {} })).toResolveTruthy(); - }); - - it('boolean optionality', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Boolean? - - @@validate(x) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { x: false } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: true } })).toResolveTruthy(); - await expect(db.model.create({ data: {} })).toResolveTruthy(); - }); - - it('optionality with binary', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int? - y Int? - - @@validate(x > y) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 1 } })).toResolveTruthy(); - await expect(db.model.create({ data: { y: 1 } })).toResolveTruthy(); - await expect(db.model.create({ data: {} })).toResolveTruthy(); - }); - - it('optionality with in operator lhs', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x String? - - @@validate(x in ['foo', 'bar']) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { x: 'hello' } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 'foo' } })).toResolveTruthy(); - await expect(db.model.create({ data: {} })).toResolveTruthy(); - }); - - it('optionality with in operator rhs', async () => { - let prisma; - try { - const dbUrl = await createPostgresDb('field-validation-in-operator'); - const r = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x String[] - - @@validate('foo' in x) - } - `, - { - provider: 'postgresql', - dbUrl, - enhancements: ['validation'], - } - ); - - const db = r.enhance(); - prisma = r.prisma; - - await expect(db.model.create({ data: { x: ['hello'] } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: ['foo', 'bar'] } })).toResolveTruthy(); - await expect(db.model.create({ data: {} })).toResolveTruthy(); - } finally { - await prisma.$disconnect(); - await dropPostgresDb('field-validation-in-operator'); - } - }); - - it('optionality with complex expression', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int? - y Int? - - @@validate(y > 1 && x > y) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { y: 1 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { y: 2 } })).toResolveTruthy(); - await expect(db.model.create({ data: {} })).toResolveTruthy(); - await expect(db.model.create({ data: { x: 1, y: 2 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: 3, y: 2 } })).toResolveTruthy(); - }); - - it('optionality with negation', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Boolean? - - @@validate(!x) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { x: true } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { x: false } })).toResolveTruthy(); - await expect(db.model.create({ data: {} })).toResolveTruthy(); - }); - - it('update implied optionality', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int - - @@validate(x > y) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { id: 1, x: 2, y: 1 } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { y: 1 } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: {} })).toResolveTruthy(); - }); - - it('optionality with scalar functions', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - s String - e String - u String - d String - - @@validate( - length(s, 1, 5) && - contains(s, 'b') && - startsWith(s, 'a') && - endsWith(s, 'c') && - regex(s, '^[0-9a-zA-Z]*$'), - 'invalid s') - @@validate(email(e), 'invalid e') - @@validate(url(u), 'invalid u') - @@validate(datetime(d), 'invalid d') - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect( - db.model.create({ - data: { - id: 1, - s: 'a1b2c', - e: 'a@bcd.com', - u: 'https://www.zenstack.dev', - d: '2024-01-01T00:00:00.000Z', - }, - }) - ).toResolveTruthy(); - - await expect(db.model.update({ where: { id: 1 }, data: {} })).toResolveTruthy(); - - await expect(db.model.update({ where: { id: 1 }, data: { s: 'a2b3c' } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { s: 'c2b3c' } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: 1 }, data: { s: 'a1b2c3' } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: 1 }, data: { s: 'aaccc' } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: 1 }, data: { s: 'a1b2d' } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: 1 }, data: { s: 'a1-3c' } })).toBeRejectedByPolicy(); - - await expect(db.model.update({ where: { id: 1 }, data: { e: 'b@def.com' } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { e: 'xyz' } })).toBeRejectedByPolicy(); - - await expect(db.model.update({ where: { id: 1 }, data: { u: 'https://zenstack.dev/docs' } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { u: 'xyz' } })).toBeRejectedByPolicy(); - - await expect(db.model.update({ where: { id: 1 }, data: { d: '2025-01-01T00:00:00.000Z' } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { d: 'xyz' } })).toBeRejectedByPolicy(); - }); - - it('optionality with array functions', async () => { - let prisma; - try { - const dbUrl = await createPostgresDb('field-validation-array-funcs'); - const r = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x String[] - y Int[] - - @@validate( - has(x, 'a') && - hasEvery(x, ['a', 'b']) && - hasSome(x, ['x', 'y']) && - (y == null || !isEmpty(y)) - ) - } - `, - { - provider: 'postgresql', - dbUrl, - enhancements: ['validation'], - } - ); - - const db = r.enhance(); - prisma = r.prisma; - - await expect(db.model.create({ data: { id: 1, x: ['a', 'b', 'x'], y: [1] } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: {} })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { x: ['b', 'x'] } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: 1 }, data: { x: ['a', 'b'] } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: 1 }, data: { y: [] } })).toBeRejectedByPolicy(); - } finally { - await prisma.$disconnect(); - await dropPostgresDb('field-validation-array-funcs'); - } - }); - - it('null comparison', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - x Int - y Int - - @@validate(x == null || !(x <= 0)) - @@validate(y != null && !(y > 1)) - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - await expect(db.model.create({ data: { id: 1, x: 1 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { id: 1, x: 1, y: 2 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { id: 1, x: 0, y: 0 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { id: 1, x: 1, y: 0 } })).toResolveTruthy(); - - await expect(db.model.update({ where: { id: 1 }, data: {} })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { y: 2 } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: 1 }, data: { y: 1 } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: 1 }, data: { x: 2, y: 1 } })).toResolveTruthy(); - }); -}); - -describe('Policy and validation interaction', () => { - it('test', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - email String? @email - age Int - - @@allow('all', age > 0) - } - ` - ); - - const db = enhance(); - - await expect( - db.user.create({ - data: { - email: 'hello', - age: 18, - }, - }) - ).toBeRejectedByPolicy(['email']); - - await expect( - db.user.create({ - data: { - email: 'user@abc.com', - age: 0, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - email: 'user@abc.com', - age: 18, - }, - }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts b/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts deleted file mode 100644 index c910ff4f1..000000000 --- a/tests/integration/tests/enhancements/with-policy/fluent-api.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: fluent API', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(async () => { - process.chdir(origDir); - }); - - it('policy tests', async () => { - const { enhance, prisma } = await loadSchema( - ` -model User { - id Int @id - email String @unique - profile Profile? - posts Post[] - - @@allow('all', true) -} - -model Profile { - id Int @id - age Int - user User @relation(fields: [userId], references: [id]) - userId Int @unique - @@allow('all', auth() == user) -} - -model Post { - id Int @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - published Boolean @default(false) - secret String @default("secret") @allow('read', published == false, true) - - @@allow('read', published) -}` - ); - - await prisma.user.create({ - data: { - id: 1, - email: 'a@test.com', - profile: { - create: { id: 1, age: 18 }, - }, - posts: { - create: [ - { id: 1, title: 'post1', published: true }, - { id: 2, title: 'post2', published: true }, - { id: 3, title: 'post3', published: false }, - ], - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - email: 'b@test.com', - posts: { - create: [{ id: 4, title: 'post4' }], - }, - }, - }); - - const db1 = enhance({ id: 1 }); - const db2 = enhance({ id: 2 }); - - // check policies - await expect(db1.user.findUnique({ where: { id: 1 } }).posts()).resolves.toHaveLength(2); - await expect(db2.user.findUnique({ where: { id: 2 } }).posts()).resolves.toHaveLength(0); - await expect( - db1.user.findUnique({ where: { id: 1 } }).posts({ where: { published: true } }) - ).resolves.toHaveLength(2); - await expect(db1.user.findUnique({ where: { id: 1 } }).posts({ take: 1 })).resolves.toHaveLength(1); - - // field-level policies - let p = ( - await db1.user - .findUnique({ where: { id: 1 } }) - .posts({ where: { published: true }, select: { secret: true } }) - )[0]; - expect(p.secret).toBeUndefined(); - p = ( - await db1.user - .findUnique({ where: { id: 1 } }) - .posts({ where: { published: false }, select: { secret: true } }) - )[0]; - expect(p.secret).toBeTruthy(); - - // to-one optional - await expect(db1.post.findFirst({ where: { id: 1 } }).author()).resolves.toMatchObject({ - id: 1, - email: 'a@test.com', - }); - await expect(db1.post.findFirst({ where: { id: 1 } }).author({ where: { id: 1 } })).resolves.toMatchObject({ - id: 1, - email: 'a@test.com', - }); - await expect(db1.post.findFirst({ where: { id: 1 } }).author({ where: { id: 2 } })).toResolveNull(); - - // to-one required - await expect(db1.profile.findUnique({ where: { userId: 1 } }).user()).resolves.toMatchObject({ - id: 1, - email: 'a@test.com', - }); - // not found - await expect(db1.profile.findUnique({ where: { userId: 2 } }).user()).toResolveNull(); - // not readable - await expect(db2.profile.findUnique({ where: { userId: 1 } }).user()).toResolveNull(); - - // unresolved promise - db1.user.findUniqueOrThrow({ where: { id: 5 } }); - db1.user.findUniqueOrThrow({ where: { id: 5 } }).posts(); - - // not-found - await expect(db1.user.findUniqueOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); - await expect(db1.user.findFirstOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); - await expect(db1.post.findUniqueOrThrow({ where: { id: 5 } }).author()).toBeNotFound(); - await expect(db1.post.findFirstOrThrow({ where: { id: 5 } }).author()).toBeNotFound(); - - // chaining - await expect( - db1.post - .findFirst({ where: { id: 1 } }) - .author() - .posts() - ).resolves.toHaveLength(2); - await expect( - db1.post - .findFirst({ where: { id: 1 } }) - .author() - .posts({ where: { published: true } }) - ).resolves.toHaveLength(2); - - // chaining broken - expect((db1.post.findMany() as any).author).toBeUndefined(); - expect( - db1.post - .findFirst({ where: { id: 1 } }) - .author() - .posts().author - ).toBeUndefined(); - }); - - it('non-policy tests', async () => { - const { enhance, prisma } = await loadSchema( - ` -model User { - id Int @id - email String @unique - password String? @omit - profile Profile? - posts Post[] -} - -model Profile { - id Int @id - age Int - user User @relation(fields: [userId], references: [id]) - userId Int @unique -} - -model Post { - id Int @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - published Boolean @default(false) -}`, - { enhancements: ['omit'] } - ); - - await prisma.user.create({ - data: { - id: 1, - email: 'a@test.com', - profile: { - create: { id: 1, age: 18 }, - }, - posts: { - create: [ - { id: 1, title: 'post1', published: true }, - { id: 2, title: 'post2', published: false }, - ], - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - email: 'b@test.com', - posts: { - create: [{ id: 3, title: 'post3' }], - }, - }, - }); - - const db = enhance(); - - // check policies - await expect(db.user.findUnique({ where: { id: 1 } }).posts()).resolves.toHaveLength(2); - await expect( - db.user.findUnique({ where: { id: 1 } }).posts({ where: { published: true } }) - ).resolves.toHaveLength(1); - await expect(db.user.findUnique({ where: { id: 1 } }).posts({ take: 1 })).resolves.toHaveLength(1); - - // to-one optional - await expect(db.post.findFirst({ where: { id: 1 } }).author()).resolves.toMatchObject({ - id: 1, - email: 'a@test.com', - }); - await expect(db.post.findFirst({ where: { id: 1 } }).author({ where: { id: 1 } })).resolves.toMatchObject({ - id: 1, - email: 'a@test.com', - }); - await expect(db.post.findFirst({ where: { id: 1 } }).author({ where: { id: 2 } })).toResolveNull(); - - // to-one required - await expect(db.profile.findUnique({ where: { userId: 1 } }).user()).resolves.toMatchObject({ - id: 1, - email: 'a@test.com', - }); - // not found - await expect(db.profile.findUnique({ where: { userId: 2 } }).user()).toResolveNull(); - - // not-found - await expect(db.user.findUniqueOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); - await expect(db.user.findFirstOrThrow({ where: { id: 5 } }).posts()).toBeNotFound(); - await expect(db.post.findUniqueOrThrow({ where: { id: 5 } }).author()).toBeNotFound(); - await expect(db.post.findFirstOrThrow({ where: { id: 5 } }).author()).toBeNotFound(); - - // chaining - await expect( - db.post - .findFirst({ where: { id: 1 } }) - .author() - .posts() - ).resolves.toHaveLength(2); - await expect( - db.post - .findFirst({ where: { id: 1 } }) - .author() - .posts({ where: { published: true } }) - ).resolves.toHaveLength(1); - - // chaining broken - expect((db.post.findMany() as any).author).toBeUndefined(); - expect( - db.post - .findFirst({ where: { id: 1 } }) - .author() - .posts().author - ).toBeUndefined(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/multi-field-unique.test.ts b/tests/integration/tests/enhancements/with-policy/multi-field-unique.test.ts deleted file mode 100644 index f0eeb1a8a..000000000 --- a/tests/integration/tests/enhancements/with-policy/multi-field-unique.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: multi-field unique', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('toplevel crud test unnamed constraint', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - a String - b String - x Int - @@unique([a, b]) - - @@allow('all', x > 0) - @@deny('update', future().x <= 0) - } - ` - ); - - const db = enhance(); - - await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy(); - await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 2 } })).toBeRejectedWithCode('P2002'); - await expect(db.model.create({ data: { a: 'a2', b: 'b2', x: 0 } })).toBeRejectedByPolicy(); - - await expect(db.model.findUnique({ where: { a_b: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); - await expect(db.model.findUnique({ where: { a_b: { a: 'a1', b: 'b2' } } })).toResolveFalsy(); - await expect(db.model.update({ where: { a_b: { a: 'a1', b: 'b1' } }, data: { x: 2 } })).toResolveTruthy(); - await expect(db.model.update({ where: { a_b: { a: 'a1', b: 'b1' } }, data: { x: 0 } })).toBeRejectedByPolicy(); - - await expect(db.model.delete({ where: { a_b: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); - }); - - it('toplevel crud test named constraint', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - a String - b String - x Int - @@unique([a, b], name: 'myconstraint') - - @@allow('all', x > 0) - @@deny('update', future().x <= 0) - } - ` - ); - - const db = enhance(); - - await expect(db.model.create({ data: { a: 'a1', b: 'b1', x: 1 } })).toResolveTruthy(); - await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); - await expect(db.model.findUnique({ where: { myconstraint: { a: 'a1', b: 'b2' } } })).toResolveFalsy(); - await expect( - db.model.update({ where: { myconstraint: { a: 'a1', b: 'b1' } }, data: { x: 2 } }) - ).toResolveTruthy(); - await expect( - db.model.update({ where: { myconstraint: { a: 'a1', b: 'b1' } }, data: { x: 0 } }) - ).toBeRejectedByPolicy(); - await expect(db.model.delete({ where: { myconstraint: { a: 'a1', b: 'b1' } } })).toResolveTruthy(); - }); - - it('nested crud test', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - a String - b String - x Int - m1 M1 @relation(fields: [m1Id], references: [id]) - m1Id String - - @@unique([a, b]) - @@allow('all', x > 0) - } - ` - ); - - const db = enhance(); - - await expect(db.m1.create({ data: { id: '1', m2: { create: { a: 'a1', b: 'b1', x: 1 } } } })).toResolveTruthy(); - await expect( - db.m1.create({ data: { id: '2', m2: { create: { a: 'a1', b: 'b1', x: 2 } } } }) - ).toBeRejectedWithCode('P2002'); - await expect( - db.m1.create({ data: { id: '3', m2: { create: { a: 'a1', b: 'b2', x: 0 } } } }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - connectOrCreate: { - where: { a_b: { a: 'a1', b: 'b1' } }, - create: { a: 'a1', b: 'b1', x: 2 }, - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(db.m2.count()).resolves.toBe(1); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - connectOrCreate: { - where: { a_b: { a: 'a1', b: 'b2' } }, - create: { a: 'a1', b: 'b2', x: 2 }, - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(db.m2.count()).resolves.toBe(2); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - connectOrCreate: { - where: { a_b: { a: 'a2', b: 'b2' } }, - create: { a: 'a2', b: 'b2', x: 0 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - update: { - where: { a_b: { a: 'a1', b: 'b2' } }, - data: { x: 3 }, - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(db.m2.findUnique({ where: { a_b: { a: 'a1', b: 'b2' } } })).resolves.toEqual( - expect.objectContaining({ x: 3 }) - ); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - delete: { - a_b: { a: 'a1', b: 'b1' }, - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(db.m2.count()).resolves.toBe(1); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/multi-file.test.ts b/tests/integration/tests/enhancements/with-policy/multi-file.test.ts deleted file mode 100644 index ade561b51..000000000 --- a/tests/integration/tests/enhancements/with-policy/multi-file.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { loadSchema, FILE_SPLITTER } from '@zenstackhq/testtools'; - -describe('Multiple file test', () => { - it('model loading', async () => { - await loadSchema( - `schema.zmodel - import "post" - ${FILE_SPLITTER}post.zmodel - import "user" - model Post { - id Int @id @default(autoincrement()) - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - - // require login - @@deny('all', auth() == null) - - // can be read by owner or space members (only if not private) - @@allow('all', owner == auth()) - } - ${FILE_SPLITTER}user.zmodel - import "post" - model User { - id Int @id @default(autoincrement()) - name String - email String @unique - posts Post[] - } - ` - ); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts b/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts deleted file mode 100644 index 0abb45559..000000000 --- a/tests/integration/tests/enhancements/with-policy/multi-id-fields.test.ts +++ /dev/null @@ -1,417 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: multiple id fields', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('multi-id fields crud', async () => { - const { prisma, enhance } = await loadSchema( - ` - model A { - x String - y Int - value Int - b B? - @@id([x, y]) - - @@allow('read', true) - @@allow('create', value > 0) - } - - model B { - b1 String - b2 String - value Int - a A @relation(fields: [ax, ay], references: [x, y]) - ax String - ay Int - - @@allow('read', value > 2) - @@allow('create', value > 1) - - @@unique([ax, ay]) - @@id([b1, b2]) - } - ` - ); - - const db = enhance(); - - await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); - await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); - - await expect( - db.a.create({ data: { x: '2', y: 1, value: 1, b: { create: { b1: '1', b2: '2', value: 1 } } } }) - ).toBeRejectedByPolicy(); - - const r = await db.a.create({ - include: { b: true }, - data: { x: '2', y: 1, value: 1, b: { create: { b1: '1', b2: '2', value: 2 } } }, - }); - expect(r.b).toBeNull(); - - const r1 = await prisma.b.findUnique({ where: { b1_b2: { b1: '1', b2: '2' } } }); - expect(r1.value).toBe(2); - - await expect( - db.a.create({ - include: { b: true }, - data: { x: '3', y: 1, value: 1, b: { create: { b1: '2', b2: '2', value: 3 } } }, - }) - ).toResolveTruthy(); - }); - - it('multi-id fields id update', async () => { - const { prisma, enhance } = await loadSchema( - ` - model A { - x String - y Int - value Int - b B? - @@id([x, y]) - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 0 && future().value > 1) - } - - model B { - b1 String - b2 String - value Int - a A @relation(fields: [ax, ay], references: [x, y]) - ax String - ay Int - - @@allow('read', value > 2) - @@allow('create', value > 1) - - @@unique([ax, ay]) - @@id([b1, b2]) - } - ` - ); - - const db = enhance(); - - await db.a.create({ data: { x: '1', y: 2, value: 1 } }); - - await expect( - db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 0 } }) - ).toBeRejectedByPolicy(); - - await expect( - db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 2 } }) - ).resolves.toMatchObject({ - x: '2', - y: 3, - value: 2, - }); - - await expect( - db.a.upsert({ - where: { x_y: { x: '2', y: 3 } }, - update: { x: '3', y: 4, value: 0 }, - create: { x: '4', y: 5, value: 5 }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.a.upsert({ - where: { x_y: { x: '2', y: 3 } }, - update: { x: '3', y: 4, value: 3 }, - create: { x: '4', y: 5, value: 5 }, - }) - ).resolves.toMatchObject({ - x: '3', - y: 4, - value: 3, - }); - }); - - it('multi-id auth', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - x String - y String - m M? - n N? - p P? - q Q? - @@id([x, y]) - @@allow('all', true) - } - - model M { - id String @id @default(cuid()) - owner User @relation(fields: [ownerX, ownerY], references: [x, y]) - ownerX String - ownerY String - @@unique([ownerX, ownerY]) - @@allow('all', auth() == owner) - } - - model N { - id String @id @default(cuid()) - owner User @relation(fields: [ownerX, ownerY], references: [x, y]) - ownerX String - ownerY String - @@unique([ownerX, ownerY]) - @@allow('all', auth().x == owner.x && auth().y == owner.y) - } - - model P { - id String @id @default(cuid()) - owner User @relation(fields: [ownerX, ownerY], references: [x, y]) - ownerX String - ownerY String - @@unique([ownerX, ownerY]) - @@allow('all', auth() != owner) - } - - model Q { - id String @id @default(cuid()) - owner User @relation(fields: [ownerX, ownerY], references: [x, y]) - ownerX String - ownerY String - @@unique([ownerX, ownerY]) - @@allow('all', auth() != null) - } - ` - ); - - await prisma.user.create({ data: { x: '1', y: '1' } }); - await prisma.user.create({ data: { x: '1', y: '2' } }); - - const anonDb = enhance(); - - await expect( - anonDb.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }) - ).toBeRejectedByPolicy(); - await expect( - anonDb.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) - ).toBeRejectedByPolicy(); - await expect( - anonDb.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }) - ).toBeRejectedByPolicy(); - await expect( - anonDb.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) - ).toBeRejectedByPolicy(); - - const db = enhance({ x: '1', y: '1' }); - - await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toBeRejectedByPolicy(); - await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toResolveTruthy(); - await expect(db.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toBeRejectedByPolicy(); - await expect(db.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toResolveTruthy(); - await expect(db.p.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toBeRejectedByPolicy(); - await expect(db.p.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); - - await expect( - enhance(undefined).q.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }) - ).toBeRejectedByPolicy(); - await expect(db.q.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); - }); - - it('multi-id to-one nested write', async () => { - const { enhance } = await loadSchema( - ` - model A { - x Int - y Int - v Int - b B @relation(fields: [bId], references: [id]) - bId Int @unique - - @@id([x, y]) - @@allow('all', v > 0) - } - - model B { - id Int @id - v Int - a A? - - @@allow('all', v > 0) - } - ` - ); - const db = enhance(); - await expect( - db.b.create({ - data: { - id: 1, - v: 1, - a: { - create: { - x: 1, - y: 2, - v: 3, - }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.a.update({ - where: { x_y: { x: 1, y: 2 } }, - data: { b: { update: { v: 5 } } }, - }) - ).toResolveTruthy(); - - expect(await db.b.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 5 })); - }); - - it('multi-id to-many nested write', async () => { - const { enhance } = await loadSchema( - ` - model A { - x Int - y Int - v Int - b B @relation(fields: [bId], references: [id]) - bId Int @unique - - @@id([x, y]) - @@allow('all', v > 0) - } - - model B { - id Int @id - v Int - a A[] - c C? - - @@allow('all', v > 0) - } - - model C { - id Int @id - v Int - b B @relation(fields: [bId], references: [id]) - bId Int @unique - - @@allow('all', v > 0) - } - ` - ); - const db = enhance(); - await expect( - db.b.create({ - data: { - id: 1, - v: 1, - a: { - create: { - x: 1, - y: 2, - v: 2, - }, - }, - c: { - create: { - id: 1, - v: 3, - }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.a.update({ - where: { x_y: { x: 1, y: 2 } }, - data: { b: { update: { v: 5, c: { update: { v: 6 } } } } }, - }) - ).toResolveTruthy(); - - expect(await db.b.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 5 })); - expect(await db.c.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 6 })); - }); - - it('multi-id fields nested id update', async () => { - const { enhance } = await loadSchema( - ` - model A { - x String - y Int - value Int - b B @relation(fields: [bId], references: [id]) - bId Int - @@id([x, y]) - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 0 && future().value > 1) - } - - model B { - id Int @id @default(autoincrement()) - a A[] - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await db.b.create({ data: { id: 1, a: { create: { x: '1', y: 1, value: 1 } } } }); - - await expect( - db.b.update({ - where: { id: 1 }, - data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 0 } } } }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.b.update({ - where: { id: 1 }, - data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 2 } } } }, - include: { a: true }, - }) - ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '2', y: 2, value: 2 })]) }); - - await expect( - db.b.update({ - where: { id: 1 }, - data: { - a: { - upsert: { - where: { x_y: { x: '2', y: 2 } }, - update: { x: '3', y: 3, value: 0 }, - create: { x: '4', y: '4', value: 4 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.b.update({ - where: { id: 1 }, - data: { - a: { - upsert: { - where: { x_y: { x: '2', y: 2 } }, - update: { x: '3', y: 3, value: 3 }, - create: { x: '4', y: '4', value: 4 }, - }, - }, - }, - include: { a: true }, - }) - ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '3', y: 3, value: 3 })]) }); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts b/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts deleted file mode 100644 index 01d2c36e2..000000000 --- a/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts +++ /dev/null @@ -1,779 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy:nested to-many', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('read filtering', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('create', true) - @@allow('read', value > 0) - } - ` - ); - - const db = enhance(); - - let read = await db.m1.create({ - include: { m2: true }, - data: { - id: '1', - m2: { - create: [{ value: 0 }], - }, - }, - }); - expect(read.m2).toHaveLength(0); - read = await db.m1.findFirst({ where: { id: '1' }, include: { m2: true } }); - expect(read.m2).toHaveLength(0); - - await db.m1.create({ - data: { - id: '2', - m2: { - create: [{ value: 0 }, { value: 1 }, { value: 2 }], - }, - }, - }); - read = await db.m1.findFirst({ where: { id: '2' }, include: { m2: true } }); - expect(read.m2).toHaveLength(2); - }); - - it('read condition hoisting', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - m3 M3 @relation(fields: [m3Id], references:[id]) - m3Id String @unique - - m4 M4 @relation(fields: [m4Id], references:[id]) - m4Id String - - @@allow('create', true) - @@allow('read', value > 0) - } - - model M3 { - id String @id @default(uuid()) - value Int - m2 M2? - - @@allow('create', true) - @@allow('read', value > 1) - } - - model M4 { - id String @id @default(uuid()) - value Int - m2 M2[] - - @@allow('create', true) - @@allow('read', value > 1) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - include: { m2: true }, - data: { - id: '1', - m2: { - create: [ - { id: 'm2-1', value: 1, m3: { create: { value: 1 } }, m4: { create: { value: 1 } } }, - { id: 'm2-2', value: 1, m3: { create: { value: 2 } }, m4: { create: { value: 2 } } }, - ], - }, - }, - }); - - let read = await db.m1.findFirst({ include: { m2: true } }); - expect(read.m2).toHaveLength(2); - read = await db.m1.findFirst({ select: { m2: { select: { id: true } } } }); - expect(read.m2).toHaveLength(2); - - // check m2-m3 filtering - // including m3 causes m2 to be filtered since m3 is not nullable - read = await db.m1.findFirst({ include: { m2: { include: { m3: true } } } }); - expect(read.m2).toHaveLength(1); - read = await db.m1.findFirst({ select: { m2: { select: { m3: true } } } }); - expect(read.m2).toHaveLength(1); - - // check m2-m4 filtering - // including m3 causes m2 to be filtered since m4 is not nullable - read = await db.m1.findFirst({ include: { m2: { include: { m4: true } } } }); - expect(read.m2).toHaveLength(1); - read = await db.m1.findFirst({ select: { m2: { select: { m4: true } } } }); - expect(read.m2).toHaveLength(1); - }); - - it('create simple', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('read', true) - @@allow('create', value > 0) - } - ` - ); - - const db = enhance(); - - // single create denied - await expect( - db.m1.create({ - data: { - m2: { - create: { value: 0 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.create({ - data: { - m2: { - create: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - // multi create denied - await expect( - db.m1.create({ - data: { - m2: { - create: [{ value: 0 }, { value: 1 }], - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.create({ - data: { - m2: { - create: [{ value: 1 }, { value: 2 }], - }, - }, - }) - ).toResolveTruthy(); - }); - - it('update simple', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('read', true) - @@allow('create', true) - @@allow('update', value > 1) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - m2: { - create: [{ id: '1', value: 1 }], - }, - }, - }); - - // update denied - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - update: { - where: { id: '1' }, - data: { value: 2 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await db.m1.create({ - data: { - id: '2', - m2: { - create: { id: '2', value: 2 }, - }, - }, - }); - - // update success - const r = await db.m1.update({ - where: { id: '2' }, - include: { m2: true }, - data: { - m2: { - update: { - where: { id: '2' }, - data: { value: 3 }, - }, - }, - }, - }); - expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '2', value: 3 })])); - }); - - it('update id field', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('read', true) - @@allow('create', true) - @@allow('update', value > 1 && future().value > 2) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - m2: { - create: { id: '1', value: 2 }, - }, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - include: { m2: true }, - data: { - m2: { - update: { - where: { id: '1' }, - data: { id: '2', value: 1 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - let r = await db.m1.update({ - where: { id: '1' }, - include: { m2: true }, - data: { - m2: { - update: { - where: { id: '1' }, - data: { id: '2', value: 3 }, - }, - }, - }, - }); - expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '2', value: 3 })])); - - await expect( - db.m1.update({ - where: { id: '1' }, - include: { m2: true }, - data: { - m2: { - upsert: { - where: { id: '2' }, - create: { id: '4', value: 4 }, - update: { id: '3', value: 1 }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - r = await db.m1.update({ - where: { id: '1' }, - include: { m2: true }, - data: { - m2: { - upsert: { - where: { id: '2' }, - create: { id: '4', value: 4 }, - update: { id: '3', value: 4 }, - }, - }, - }, - }); - expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '3', value: 4 })])); - }); - - it('update with create from one to many', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 1) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - m2: { - create: { value: 1 }, - }, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - create: [{ value: 0 }, { value: 1 }], - }, - }, - }) - ).toBeRejectedByPolicy(); - - const r = await db.m1.update({ - where: { id: '1' }, - include: { m2: true }, - data: { - m2: { - create: [{ value: 1 }, { value: 2 }], - }, - }, - }); - expect(r.m2).toHaveLength(3); - }); - - it('update with create from many to one', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - value Int - m2 M2[] - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 1) - } - - model M2 { - id String @id @default(uuid()) - m1 M1? @relation(fields: [m1Id], references:[id]) - m1Id String? - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await db.m2.create({ data: { id: '1' } }); - - await expect( - db.m2.update({ - where: { id: '1' }, - data: { - m1: { - create: { value: 0 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m2.update({ - where: { id: '1' }, - data: { - m1: { - create: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('update with delete', async () => { - const { enhance, prisma } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 1) - @@allow('delete', value > 2) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { id: '1', value: 1 }, - { id: '2', value: 2 }, - { id: '3', value: 3 }, - { id: '4', value: 4 }, - { id: '5', value: 5 }, - ], - }, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - delete: { id: '1' }, - }, - }, - }) - ).toBeRejectedByPolicy(); - expect(await prisma.m2.findMany()).toHaveLength(5); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - delete: [{ id: '1' }, { id: '2' }], - }, - }, - }) - ).toBeRejectedByPolicy(); - expect(await prisma.m2.findMany()).toHaveLength(5); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - deleteMany: { OR: [{ id: '2' }, { id: '3' }] }, - }, - }, - }) - ).toResolveTruthy(); - // only m2#3 should be deleted, m2#2 should remain because of policy - await expect(db.m2.findUnique({ where: { id: '3' } })).toResolveNull(); - await expect(db.m2.findUnique({ where: { id: '2' } })).toResolveTruthy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - delete: { id: '3' }, - }, - }, - }) - ).toBeNotFound(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - deleteMany: { value: { gte: 4 } }, - }, - }, - }) - ).toResolveTruthy(); - - await expect(db.m2.findMany({ where: { id: { in: ['4', '5'] } } })).resolves.toHaveLength(0); - }); - - it('create with nested read', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - value Int - m2 M2[] - m3 M3? - - @@allow('read', value > 1) - @@allow('create', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('create', true) - @@allow('read', value > 0) - } - - model M3 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('create', true) - @@allow('read', value > 0) - } - ` - ); - - const db = enhance(); - - await expect( - db.m1.create({ - data: { - id: '1', - value: 1, - }, - }) - ).toBeRejectedByPolicy(); - - // included 'm1' can't be read - await expect( - db.m2.create({ - include: { m1: true }, - data: { - id: '1', - value: 1, - m1: { connect: { id: '1' } }, - }, - }) - ).toBeRejectedByPolicy(); - await expect(db.m2.findUnique({ where: { id: '1' } })).toResolveTruthy(); - - // included 'm1' can't be read - await expect( - db.m3.create({ - include: { m1: true }, - data: { - id: '1', - value: 1, - m1: { connect: { id: '1' } }, - }, - }) - ).toBeRejectedByPolicy(); - await expect(db.m3.findUnique({ where: { id: '1' } })).toResolveTruthy(); - - // nested to-many got filtered on read - const r = await db.m1.create({ - include: { m2: true }, - data: { - value: 2, - m2: { create: [{ value: 0 }, { value: 1 }] }, - }, - }); - expect(r.m2).toHaveLength(1); - - // read-back for to-one relation rejected - const r1 = await db.m1.create({ - include: { m3: true }, - data: { - value: 2, - m3: { create: { value: 0 } }, - }, - }); - expect(r1.m3).toBeNull(); - }); - - it('update with nested read', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - m3 M3? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('read', value > 1) - @@allow('create,update', true) - } - - model M3 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('read', value > 1) - @@allow('create,update', true) - } - ` - ); - - const db = enhance(); - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { id: '1', value: 0 }, - { id: '2', value: 0 }, - ], - }, - m3: { - create: { value: 0 }, - }, - }, - }); - - const r = await db.m1.update({ - where: { id: '1' }, - include: { m3: true }, - data: { - m3: { - update: { - value: 1, - }, - }, - }, - }); - expect(r.m3).toBeNull(); - - const r1 = await db.m1.update({ - where: { id: '1' }, - include: { m3: true, m2: true }, - data: { - m3: { - update: { - value: 2, - }, - }, - }, - }); - // m3 is ok now - expect(r1.m3.value).toBe(2); - // m2 got filtered - expect(r1.m2).toHaveLength(0); - - const r2 = await db.m1.update({ - where: { id: '1' }, - select: { m2: true }, - data: { - m2: { - update: { - where: { id: '1' }, - data: { value: 2 }, - }, - }, - }, - }); - // one of m2 matches policy now - expect(r2.m2).toHaveLength(1); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts b/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts deleted file mode 100644 index 59c968fb5..000000000 --- a/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy:nested to-one', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('read filtering for optional relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - value Int - - @@allow('create', true) - @@allow('read', value > 0) - } - ` - ); - - const db = enhance(); - - let read = await db.m1.create({ - include: { m2: true }, - data: { - id: '1', - m2: { - create: { id: '1', value: 0 }, - }, - }, - }); - expect(read.m2).toBeNull(); - - await expect(db.m1.findUnique({ where: { id: '1' }, include: { m2: true } })).resolves.toEqual( - expect.objectContaining({ m2: null }) - ); - await expect(db.m1.findMany({ include: { m2: true } })).resolves.toEqual( - expect.arrayContaining([expect.objectContaining({ m2: null })]) - ); - - await prisma.m2.update({ where: { id: '1' }, data: { value: 1 } }); - read = await db.m1.findUnique({ where: { id: '1' }, include: { m2: true } }); - expect(read.m2).toEqual(expect.objectContaining({ id: '1', value: 1 })); - }); - - it('read rejection for non-optional relation', async () => { - const { prisma, enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - value Int - - @@allow('create', true) - @@allow('read', value > 0) - } - - model M2 { - id String @id @default(uuid()) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('all', true) - } - ` - ); - - await prisma.m1.create({ - data: { - id: '1', - value: 0, - m2: { - create: { id: '1' }, - }, - }, - }); - - const db = enhance(); - await expect(db.m2.findUnique({ where: { id: '1' }, include: { m1: true } })).toResolveFalsy(); - await expect(db.m2.findMany({ include: { m1: true } })).resolves.toHaveLength(0); - - await prisma.m1.update({ where: { id: '1' }, data: { value: 1 } }); - await expect(db.m2.findMany({ include: { m1: true } })).toResolveTruthy(); - }); - - it('read condition hoisting', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id String @unique - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - - m1 M1? - - m3 M3 @relation(fields: [m3Id], references:[id]) - m3Id String @unique - - @@allow('create', true) - @@allow('read', value > 0) - } - - model M3 { - id String @id @default(uuid()) - value Int - m2 M2? - - @@allow('create', true) - @@allow('read', value > 1) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - include: { m2: true }, - data: { - id: '1', - m2: { - create: { id: 'm2-1', value: 1, m3: { create: { value: 1 } } }, - }, - }, - }); - - // check m2-m3 filtering - // including m3 causes m1 to be filtered due to hosting - await expect(db.m1.findFirst({ include: { m2: { include: { m3: true } } } })).toResolveNull(); - await expect(db.m1.findFirst({ select: { m2: { select: { m3: true } } } })).toResolveNull(); - }); - - it('create and update tests', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 1) - } - ` - ); - - const db = enhance(); - - // create denied - await expect( - db.m1.create({ - data: { - m2: { - create: { value: 0 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.create({ - data: { - id: '1', - m2: { - create: { id: '1', value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - // nested update denied - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - update: { value: 2 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); - - it('nested update id tests', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 1 && future().value > 2) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - m2: { - create: { id: '1', value: 2 }, - }, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - update: { id: '2', value: 1 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - update: { id: '2', value: 3 }, - }, - }, - include: { m2: true }, - }) - ).resolves.toMatchObject({ m2: expect.objectContaining({ id: '2', value: 3 }) }); - }); - - it('nested create', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('read', true) - @@allow('create', value > 0) - @@allow('update', value > 1) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - }, - }); - - // nested create denied - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - create: { value: 0 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - create: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('nested delete', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('read', true) - @@allow('create', true) - @@allow('update', true) - @@allow('delete', value > 1) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - m2: { - create: { id: '1', value: 1 }, - }, - }, - }); - - // nested delete denied - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { delete: true }, - }, - }) - ).toBeRejectedByPolicy(); - expect(await db.m2.findUnique({ where: { id: '1' } })).toBeTruthy(); - - // update m2 so it can be deleted - await db.m1.update({ - where: { id: '1' }, - data: { - m2: { update: { value: 3 } }, - }, - }); - - expect( - await db.m1.update({ - where: { id: '1' }, - data: { - m2: { delete: true }, - }, - }) - ).toBeTruthy(); - // check deleted - expect(await db.m2.findUnique({ where: { id: '1' } })).toBeNull(); - }); - - it('nested relation delete', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - m1 M1? - - @@allow('all', true) - } - - model M1 { - id String @id @default(uuid()) - value Int - user User? @relation(fields: [userId], references: [id]) - userId String? @unique - - @@allow('read,create,update', true) - @@allow('delete', auth().id == 'user1' && value > 0) - } - ` - ); - - await enhance({ id: 'user1' }).m1.create({ - data: { - id: 'm1', - value: 1, - }, - }); - - await expect( - enhance({ id: 'user2' }).user.create({ - data: { - id: 'user2', - m1: { - connect: { id: 'm1' }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - enhance({ id: 'user2' }).user.update({ - where: { id: 'user2' }, - data: { - m1: { delete: true }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - enhance({ id: 'user1' }).user.create({ - data: { - id: 'user1', - m1: { - connect: { id: 'm1' }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - enhance({ id: 'user1' }).user.update({ - where: { id: 'user1' }, - data: { - m1: { delete: true }, - }, - }) - ).toResolveTruthy(); - - expect(await prisma.m1.findMany()).toHaveLength(0); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/options.test.ts b/tests/integration/tests/enhancements/with-policy/options.test.ts deleted file mode 100644 index 3f63f54a3..000000000 --- a/tests/integration/tests/enhancements/with-policy/options.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('Password test', () => { - it('load path', async () => { - const { prisma, projectDir } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - x Int - - @@allow('read', true) - @@allow('create', x > 0) - }`, - { getPrismaOnly: true, output: './zen' } - ); - - const enhance = require(path.join(projectDir, 'zen/enhance')).enhance; - const db = enhance(prisma); - await expect( - db.foo.create({ - data: { x: 0 }, - }) - ).toBeRejectedByPolicy(); - await expect( - db.foo.create({ - data: { x: 1 }, - }) - ).toResolveTruthy(); - }); - - it('overrides', async () => { - const { prisma, projectDir } = await loadSchema( - ` - model Foo { - id String @id @default(cuid()) - x Int - - @@allow('create', x > 0) - }`, - { getPrismaOnly: true, output: './zen' } - ); - - const enhance = require(path.join(projectDir, 'zen/enhance')).enhance; - const db = enhance(prisma, { - modelMeta: require(path.join(projectDir, 'zen/model-meta')).default, - policy: require(path.resolve(projectDir, 'zen/policy')).default, - }); - await expect( - db.foo.create({ - data: { x: 0 }, - }) - ).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts b/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts deleted file mode 100644 index 691c6176a..000000000 --- a/tests/integration/tests/enhancements/with-policy/petstore-sample.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { AuthUser } from '@zenstackhq/runtime'; -import { loadSchemaFromFile, run, type FullDbClientContract } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('Pet Store Policy Tests', () => { - let getDb: (user?: AuthUser) => FullDbClientContract; - let prisma: FullDbClientContract; - - beforeAll(async () => { - const { enhance, prisma: _prisma } = await loadSchemaFromFile( - path.join(__dirname, '../../schema/petstore.zmodel'), - { addPrelude: false } - ); - getDb = enhance; - prisma = _prisma; - }); - - beforeEach(() => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - }); - - it('crud', async () => { - const petData = [ - { - id: 'luna', - name: 'Luna', - category: 'kitten', - }, - { - id: 'max', - name: 'Max', - category: 'doggie', - }, - { - id: 'cooper', - name: 'Cooper', - category: 'reptile', - }, - ]; - - for (const pet of petData) { - await prisma.pet.create({ data: pet }); - } - - await prisma.user.create({ data: { id: 'user1', email: 'user1@abc.com' } }); - - const r = await getDb({ id: 'user1' }).order.create({ - include: { user: true, pets: true }, - data: { - user: { connect: { id: 'user1' } }, - pets: { connect: [{ id: 'luna' }, { id: 'max' }] }, - }, - }); - - expect(r.user.id).toBe('user1'); - expect(r.pets).toHaveLength(2); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/post-update.test.ts b/tests/integration/tests/enhancements/with-policy/post-update.test.ts deleted file mode 100644 index 21681bee5..000000000 --- a/tests/integration/tests/enhancements/with-policy/post-update.test.ts +++ /dev/null @@ -1,598 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: post update', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('simple allow', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('create,read', true) - @@allow('update', future().value > 1) - } - ` - ); - - const db = enhance(); - - await expect(db.model.create({ data: { id: '1', value: 0 } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: '1' }, data: { value: 1 } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: '1' }, data: { value: 2 } })).toResolveTruthy(); - }); - - it('simple deny', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('all', true) - @@deny('update', future().value <= 1) - } - ` - ); - - const db = enhance(); - - await expect(db.model.create({ data: { id: '1', value: 0 } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: '1' }, data: { value: 1 } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: '1' }, data: { value: 2 } })).toResolveTruthy(); - }); - - it('mixed pre and post', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('create,read', true) - @@allow('update', value > 0 && future().value > value) - } - ` - ); - - const db = enhance(); - - await expect(db.model.create({ data: { id: '1', value: 0 } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: '1' }, data: { value: 1 } })).toBeRejectedByPolicy(); - - await expect(db.model.create({ data: { id: '2', value: 3 } })).toResolveTruthy(); - await expect(db.model.update({ where: { id: '2' }, data: { value: 2 } })).toBeRejectedByPolicy(); - await expect(db.model.update({ where: { id: '2' }, data: { value: 4 } })).toResolveTruthy(); - }); - - it('functions pre-update', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value String - x Int - - @@allow('create,read', true) - @@allow('update', startsWith(value, 'hello') && future().x > 0) - } - ` - ); - - const db = enhance(); - - await prisma.model.create({ data: { id: '1', value: 'good', x: 1 } }); - await expect(db.model.update({ where: { id: '1' }, data: { value: 'hello' } })).toBeRejectedByPolicy(); - - await prisma.model.update({ where: { id: '1' }, data: { value: 'hello world' } }); - const r = await db.model.update({ where: { id: '1' }, data: { value: 'hello new world' } }); - expect(r.value).toBe('hello new world'); - }); - - it('functions post-update', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value String - x Int - - @@allow('create,read', true) - @@allow('update', x > 0 && startsWith(future().value, 'hello')) - } - ` - ); - - const db = enhance(); - - await prisma.model.create({ data: { id: '1', value: 'good', x: 1 } }); - await expect(db.model.update({ where: { id: '1' }, data: { value: 'nice' } })).toBeRejectedByPolicy(); - - const r = await db.model.update({ where: { id: '1' }, data: { x: 0, value: 'hello world' } }); - expect(r.value).toBe('hello world'); - }); - - it('collection predicate pre-update', async () => { - const { prisma, enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - value Int - m2 M2[] - @@allow('read', true) - @@allow('update', m2?[value > 0] && future().value > 0) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await prisma.m1.create({ - data: { - id: '1', - value: 0, - m2: { - create: [{ id: '1', value: 0 }], - }, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { value: 1 }, - }) - ).toBeRejectedByPolicy(); - - await prisma.m2.create({ - data: { - id: '2', - m1: { connect: { id: '1' } }, - value: 1, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { value: 1 }, - }) - ).toResolveTruthy(); - }); - - it('collection predicate post-update', async () => { - const { prisma, enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - value Int - m2 M2[] - @@allow('read', true) - @@allow('update', value > 0 && future().m2?[value > 0]) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await prisma.m1.create({ - data: { - id: '1', - value: 1, - m2: { - create: [{ id: '1', value: 0 }], - }, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { value: 2 }, - }) - ).toBeRejectedByPolicy(); - - await prisma.m2.create({ - data: { - id: '2', - m1: { connect: { id: '1' } }, - value: 1, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { value: 2 }, - }) - ).toResolveTruthy(); - }); - - it('collection predicate deep-nested post-update', async () => { - const { prisma, enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - value Int - m2 M2? - @@allow('read', true) - @@allow('update', value > 0 && future().m2.m3?[value > 0]) - } - - model M2 { - id String @id @default(uuid()) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - m3 M3[] - @@allow('all', true) - } - - model M3 { - id String @id @default(uuid()) - value Int - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id String - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await prisma.m1.create({ - data: { - id: '1', - value: 1, - m2: { - create: { id: '1', m3: { create: [{ id: '1', value: 0 }] } }, - }, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { value: 2 }, - }) - ).toBeRejectedByPolicy(); - - await prisma.m3.create({ - data: { - id: '2', - m2: { connect: { id: '1' } }, - value: 1, - }, - }); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { value: 2 }, - }) - ).toResolveTruthy(); - }); - - it('nested to-many', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('create,read', true) - @@allow('update', future().value > 1) - } - ` - ); - - const db = enhance(); - - await expect( - db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { id: '1', value: 0 }, - { id: '2', value: 1 }, - ], - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { m2: { updateMany: { where: {}, data: { value: { increment: 1 } } } } }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { m2: { updateMany: { where: { value: { gte: 1 } }, data: { value: { increment: 1 } } } } }, - }) - ).toResolveTruthy(); - - await expect(db.m2.findMany()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: '1', value: 0 }), - expect.objectContaining({ id: '2', value: 2 }), - ]) - ); - }); - - it('nested to-one', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('create,read', true) - @@allow('update', future().value > 1) - } - ` - ); - - const db = enhance(); - - await expect( - db.m1.create({ - data: { - id: '1', - m2: { - create: { id: '1', value: 0 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { m2: { update: { value: { increment: 1 } } } }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { m2: { update: { value: { increment: 2 } } } }, - }) - ).toResolveTruthy(); - - await expect(db.m2.findMany()).resolves.toEqual( - expect.arrayContaining([expect.objectContaining({ value: 2 })]) - ); - }); - - it('nested select', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - @@allow('create,read', true) - @@allow('update', future().m2.value > m2.value) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await expect( - db.m1.create({ - data: { - id: '1', - m2: { - create: { id: '1', value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { m2: { update: { value: 0 } } }, - }) - ).toResolveTruthy(); // m2 updatable - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { m2: { update: { value: 2 } } }, - }) - ).toResolveTruthy(); - - await expect(db.m2.findFirst()).resolves.toEqual(expect.objectContaining({ value: 2 })); - }); - - it('deep nesting', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id String @id @default(uuid()) - m2 M2? - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - m3 M3[] - - @@allow('create,read', true) - @@allow('update', future().value > 1) - } - - model M3 { - id String @id @default(uuid()) - value Int - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id String - - @@allow('create,read', true) - @@allow('update', future().value > 2) - } - ` - ); - - const db = enhance(); - - await expect( - db.m1.create({ - data: { - id: '1', - m2: { - create: { - id: '1', - value: 0, - m3: { - create: [ - { id: '1', value: 0 }, - { id: '2', value: 1 }, - ], - }, - }, - }, - }, - }) - ).toResolveTruthy(); - - // rejected because nested m3 update fails post-update rule - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { update: { value: 2, m3: { updateMany: { where: {}, data: { value: { increment: 2 } } } } } }, - }, - }) - ).toBeRejectedByPolicy(); - - // rejected because nested m2 update fails post-update rule - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { update: { value: 1, m3: { updateMany: { where: {}, data: { value: { increment: 3 } } } } } }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { update: { value: 2, m3: { updateMany: { where: {}, data: { value: { increment: 3 } } } } } }, - }, - }) - ).toResolveTruthy(); - - await expect(db.m2.findFirst()).resolves.toEqual(expect.objectContaining({ value: 2 })); - await expect(db.m3.findMany()).resolves.toEqual( - expect.arrayContaining([expect.objectContaining({ value: 3 }), expect.objectContaining({ value: 4 })]) - ); - }); - - it('deep member access', async () => { - const { enhance } = await loadSchema( - ` - model M1 { - id Int @id @default(autoincrement()) - m2 M2? - v1 Int - @@allow('all', true) - @@deny('update', future().m2.m3.v3 > 1) - } - - model M2 { - id Int @id @default(autoincrement()) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id Int @unique - m3 M3? - @@allow('all', true) - } - - model M3 { - id Int @id @default(autoincrement()) - v3 Int - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id Int @unique - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await db.m1.create({ - data: { id: 1, v1: 1, m2: { create: { id: 1, m3: { create: { id: 1, v3: 1 } } } } }, - }); - - await db.m1.create({ - data: { id: 2, v1: 2, m2: { create: { id: 2, m3: { create: { id: 2, v3: 2 } } } } }, - }); - - await expect(db.m1.update({ where: { id: 1 }, data: { v1: 2 } })).toResolveTruthy(); - await expect(db.m1.update({ where: { id: 2 }, data: { v1: 3 } })).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/postgres.test.ts b/tests/integration/tests/enhancements/with-policy/postgres.test.ts deleted file mode 100644 index f0eff7fd4..000000000 --- a/tests/integration/tests/enhancements/with-policy/postgres.test.ts +++ /dev/null @@ -1,527 +0,0 @@ -import { AuthUser } from '@zenstackhq/runtime'; -import { createPostgresDb, dropPostgresDb, loadSchemaFromFile, type FullDbClientContract } from '@zenstackhq/testtools'; -import path from 'path'; - -const DB_NAME = 'todo-pg'; - -describe('With Policy: with postgres', () => { - let origDir: string; - let dbUrl: string; - let getDb: (user?: AuthUser) => FullDbClientContract; - let prisma: FullDbClientContract; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - beforeEach(async () => { - dbUrl = await createPostgresDb(DB_NAME); - const { prisma: _prisma, enhance } = await loadSchemaFromFile( - path.join(__dirname, '../../schema/todo-pg.zmodel'), - { - provider: 'postgresql', - dbUrl, - } - ); - getDb = enhance; - prisma = _prisma; - }); - - afterEach(async () => { - process.chdir(origDir); - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb(DB_NAME); - }); - - it('user', async () => { - const user1 = { - id: 'user1', - email: 'user1@zenstack.dev', - name: 'User 1', - }; - const user2 = { - id: 'user2', - email: 'user2@zenstack.dev', - name: 'User 2', - }; - - const anonDb = getDb(); - const user1Db = getDb({ id: user1.id }); - const user2Db = getDb({ id: user2.id }); - - // create user1 - // create should succeed but result can be read back anonymously - 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(); - - // create user2 - await expect(anonDb.user.create({ data: user2 })).toBeRejectedByPolicy(); - - // find with user1 should only get user1 - const r = await user1Db.user.findMany(); - expect(r).toHaveLength(1); - expect(r[0]).toEqual(expect.objectContaining(user1)); - - // get user2 as user1 - await expect(user1Db.user.findUnique({ where: { id: user2.id } })).toResolveNull(); - - // add both users into the same space - await expect( - user1Db.space.create({ - data: { - name: 'Space 1', - slug: 'space1', - owner: { connect: { id: user1.id } }, - members: { - create: [ - { - user: { connect: { id: user1.id } }, - role: 'ADMIN', - }, - { - user: { connect: { id: user2.id } }, - role: 'USER', - }, - ], - }, - }, - }) - ).toResolveTruthy(); - - // now both user1 and user2 should be visible - await expect(user1Db.user.findMany()).resolves.toHaveLength(2); - await expect(user2Db.user.findMany()).resolves.toHaveLength(2); - - // update user2 as user1 - await expect( - user2Db.user.update({ - where: { id: user1.id }, - data: { name: 'hello' }, - }) - ).toBeRejectedByPolicy(); - - // update user1 as user1 - await expect( - user1Db.user.update({ - where: { id: user1.id }, - data: { name: 'hello' }, - }) - ).toResolveTruthy(); - - // delete user2 as user1 - await expect(user1Db.user.delete({ where: { id: user2.id } })).toBeRejectedByPolicy(); - - // delete user1 as user1 - await expect(user1Db.user.delete({ where: { id: user1.id } })).toResolveTruthy(); - await expect(user1Db.user.findUnique({ where: { id: user1.id } })).toResolveNull(); - }); - - it('todo list', async () => { - await createSpaceAndUsers(prisma); - - const anonDb = getDb(); - const emptyUIDDb = getDb({ id: '' }); - const user1Db = getDb({ id: user1.id }); - const user2Db = getDb({ id: user2.id }); - const user3Db = getDb({ id: user3.id }); - - await expect( - anonDb.list.create({ - data: { - id: 'list1', - title: 'List 1', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - user1Db.list.create({ - data: { - id: 'list1', - title: 'List 1', - 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(); - - // accessible to owner - 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(); - - // inaccessible to user not in the space - await expect(user3Db.list.findUnique({ where: { id: 'list1' } })).toResolveNull(); - - // make a private list - await user1Db.list.create({ - data: { - id: 'list2', - title: 'List 2', - private: true, - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - // accessible to owner - 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(); - - // create a list which doesn't match credential should fail - await expect( - user1Db.list.create({ - data: { - id: 'list3', - title: 'List 3', - owner: { connect: { id: user2.id } }, - space: { connect: { id: space1.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - // create a list which doesn't match credential's space should fail - await expect( - user1Db.list.create({ - data: { - id: 'list3', - title: 'List 3', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space2.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - // update list - await expect( - user1Db.list.update({ - where: { id: 'list1' }, - data: { - title: 'List 1 updated', - }, - }) - ).resolves.toEqual(expect.objectContaining({ title: 'List 1 updated' })); - - await expect( - user2Db.list.update({ - where: { id: 'list1' }, - data: { - title: 'List 1 updated', - }, - }) - ).toBeRejectedByPolicy(); - - // delete list - await expect(user2Db.list.delete({ where: { id: 'list1' } })).toBeRejectedByPolicy(); - await expect(user1Db.list.delete({ where: { id: 'list1' } })).toResolveTruthy(); - await expect(user1Db.list.findUnique({ where: { id: 'list1' } })).toResolveNull(); - }); - - it('todo', async () => { - await createSpaceAndUsers(prisma); - - const user1Db = getDb({ id: user1.id }); - const user2Db = getDb({ id: user2.id }); - - // create a public list - await user1Db.list.create({ - data: { - id: 'list1', - title: 'List 1', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - // create - await expect( - user1Db.todo.create({ - data: { - id: 'todo1', - title: 'Todo 1', - owner: { connect: { id: user1.id } }, - list: { - connect: { id: 'list1' }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - user2Db.todo.create({ - data: { - id: 'todo2', - title: 'Todo 2', - owner: { connect: { id: user2.id } }, - list: { - connect: { id: 'list1' }, - }, - }, - }) - ).toResolveTruthy(); - - // read - await expect(user1Db.todo.findMany()).resolves.toHaveLength(2); - await expect(user2Db.todo.findMany()).resolves.toHaveLength(2); - - // update, user in the same space can freely update - await expect( - user1Db.todo.update({ - where: { id: 'todo1' }, - data: { - title: 'Todo 1 updated', - }, - }) - ).toResolveTruthy(); - await expect( - user1Db.todo.update({ - where: { id: 'todo2' }, - data: { - title: 'Todo 2 updated', - }, - }) - ).toResolveTruthy(); - - // create a private list - await user1Db.list.create({ - data: { - id: 'list2', - private: true, - title: 'List 2', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - // create - await expect( - user1Db.todo.create({ - data: { - id: 'todo3', - title: 'Todo 3', - owner: { connect: { id: user1.id } }, - list: { - connect: { id: 'list2' }, - }, - }, - }) - ).toResolveTruthy(); - - // reject because list2 is private - await expect( - user2Db.todo.create({ - data: { - id: 'todo4', - title: 'Todo 4', - owner: { connect: { id: user2.id } }, - list: { - connect: { id: 'list2' }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // update, only owner can update todo in a private list - await expect( - user1Db.todo.update({ - where: { id: 'todo3' }, - data: { - title: 'Todo 3 updated', - }, - }) - ).toResolveTruthy(); - await expect( - user2Db.todo.update({ - where: { id: 'todo3' }, - data: { - title: 'Todo 3 updated', - }, - }) - ).toBeRejectedByPolicy(); - }); - - it('relation query', async () => { - await createSpaceAndUsers(prisma); - - const user1Db = getDb({ id: user1.id }); - const user2Db = getDb({ id: user2.id }); - - await user1Db.list.create({ - data: { - id: 'list1', - title: 'List 1', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - await user1Db.list.create({ - data: { - id: 'list2', - title: 'List 2', - private: true, - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - const r = await user1Db.space.findFirst({ - where: { id: 'space1' }, - include: { lists: true }, - }); - expect(r.lists).toHaveLength(2); - - const r1 = await user2Db.space.findFirst({ - where: { id: 'space1' }, - include: { lists: true }, - }); - expect(r1.lists).toHaveLength(1); - }); - - it('post-update checks', async () => { - await createSpaceAndUsers(prisma); - - const user1Db = getDb({ id: user1.id }); - - await user1Db.list.create({ - data: { - id: 'list1', - title: 'List 1', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - todos: { - create: { - id: 'todo1', - title: 'Todo 1', - owner: { connect: { id: user1.id } }, - }, - }, - }, - }); - - // change list's owner - await expect( - user1Db.list.update({ - where: { id: 'list1' }, - data: { - owner: { connect: { id: user2.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - // change todo's owner - await expect( - user1Db.todo.update({ - where: { id: 'todo1' }, - data: { - owner: { connect: { id: user2.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - // nested change todo's owner - await expect( - user1Db.list.update({ - where: { id: 'list1' }, - data: { - todos: { - update: { - where: { id: 'todo1' }, - data: { - owner: { connect: { id: user2.id } }, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); -}); - -const user1 = { - id: 'user1', - email: 'user1@zenstack.dev', - name: 'User 1', -}; - -const user2 = { - id: 'user2', - email: 'user2@zenstack.dev', - name: 'User 2', -}; - -const user3 = { - id: 'user3', - email: 'user3@zenstack.dev', - name: 'User 3', -}; - -const space1 = { - id: 'space1', - name: 'Space 1', - slug: 'space1', -}; - -const space2 = { - id: 'space2', - name: 'Space 2', - slug: 'space2', -}; - -async function createSpaceAndUsers(db: FullDbClientContract) { - // create users - await db.user.create({ data: user1 }); - await db.user.create({ data: user2 }); - await db.user.create({ data: user3 }); - - // add user1 and user2 into space1 - await db.space.create({ - data: { - ...space1, - members: { - create: [ - { - user: { connect: { id: user1.id } }, - role: 'ADMIN', - }, - { - user: { connect: { id: user2.id } }, - role: 'USER', - }, - ], - }, - }, - }); - - // add user3 to space2 - await db.space.create({ - data: { - ...space2, - members: { - create: [ - { - user: { connect: { id: user3.id } }, - role: 'ADMIN', - }, - ], - }, - }, - }); -} diff --git a/tests/integration/tests/enhancements/with-policy/prisma-omit.test.ts b/tests/integration/tests/enhancements/with-policy/prisma-omit.test.ts deleted file mode 100644 index 0ce17e66f..000000000 --- a/tests/integration/tests/enhancements/with-policy/prisma-omit.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('prisma omit', () => { - it('per query', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - name String - profile Profile? - age Int - value Int @allow('read', age > 20) - @@allow('all', age > 18) - } - - model Profile { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String @unique - level Int - @@allow('all', level > 1) - } - `, - { previewFeatures: ['omitApi'] } - ); - - await prisma.user.create({ - data: { - name: 'John', - age: 25, - value: 10, - profile: { - create: { level: 2 }, - }, - }, - }); - - const db = enhance(); - let found = await db.user.findFirst({ - include: { profile: { omit: { level: true } } }, - omit: { - age: true, - }, - }); - expect(found.age).toBeUndefined(); - expect(found.value).toEqual(10); - expect(found.profile.level).toBeUndefined(); - - found = await db.user.findFirst({ - select: { value: true, profile: { omit: { level: true } } }, - }); - console.log(found); - expect(found.age).toBeUndefined(); - expect(found.value).toEqual(10); - expect(found.profile.level).toBeUndefined(); - }); - - it('global', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - name String - profile Profile? - age Int - value Int - @@allow('all', true) - } - - model Profile { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String @unique - level Int - @@allow('all', true) - } - `, - { - previewFeatures: ['omitApi'], - prismaClientOptions: { - omit: { - user: { age: true, value: false }, - profile: { level: true }, - }, - }, - } - ); - - await prisma.user.create({ - data: { - name: 'John', - age: 25, - value: 10, - profile: { - create: { level: 2 }, - }, - }, - }); - - const db = enhance(); - - let found = await db.user.findFirst({ - include: { profile: true }, - }); - expect(found.age).toBeUndefined(); - expect(found.value).toEqual(10); - expect(found.profile.level).toBeUndefined(); - - found = await db.user.findFirst({ - omit: { age: false }, - include: { profile: true }, - }); - expect(found.age).toBe(25); - - found = await db.user.findFirst({ - select: { value: true, profile: true }, - }); - console.log(found); - expect(found.age).toBeUndefined(); - expect(found.value).toEqual(10); - expect(found.profile.level).toBeUndefined(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/prisma-pulse.test.ts b/tests/integration/tests/enhancements/with-policy/prisma-pulse.test.ts deleted file mode 100644 index 62a1bbbe5..000000000 --- a/tests/integration/tests/enhancements/with-policy/prisma-pulse.test.ts +++ /dev/null @@ -1,373 +0,0 @@ -/* eslint-disable jest/no-conditional-expect */ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -const PULSE_DB_URL = process.env.PULSE_DB_URL; -const PULSE_API_KEY = process.env.PULSE_API_KEY; - -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('With Policy: prisma pulse test', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('should conform to auth check', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - } - - model Post { - id Int @id @default(autoincrement()) - name String - - @@allow('read', auth() != null) - } - `, - { - provider: 'postgresql', - dbUrl: PULSE_DB_URL, - pulseApiKey: PULSE_API_KEY, - logPrismaQuery: true, - } - ); - - await prisma.$queryRaw`ALTER TABLE public."Post" REPLICA IDENTITY FULL;`; - await prisma.post.deleteMany(); - - const rawSub = await prisma.post.stream(); - - const anonDb = enhance(); - console.log('Anonymous db subscribing'); - const anonSub = await anonDb.post.stream(); - - const authDb = enhance({ id: 1 }); - console.log('Auth db subscribing'); - const authSub = await authDb.post.stream(); - - async function produce() { - await prisma.post.create({ data: { id: 1, name: 'abc' } }); - console.log('created'); - await prisma.post.update({ where: { id: 1 }, data: { name: 'bcd' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 1 } }); - console.log('deleted'); - await new Promise((resolve) => setTimeout(resolve, 3000)); - } - - const rawEvents: any[] = []; - const authEvents: any[] = []; - const anonEvents: any[] = []; - await Promise.race([ - produce(), - consume(rawSub, 'Raw', rawEvents), - consume(authSub, 'Auth', authEvents), - consume(anonSub, 'Anonymous', anonEvents), - ]); - expect(rawEvents.length).toBe(3); - expect(authEvents.length).toBe(3); - expect(anonEvents.length).toBe(0); - }); - - it('should conform to model-level policy', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - name String - - @@allow('read', contains(name, 'hello')) - } - `, - { - provider: 'postgresql', - dbUrl: PULSE_DB_URL, - pulseApiKey: PULSE_API_KEY, - logPrismaQuery: true, - } - ); - - await prisma.$queryRaw`ALTER TABLE public."Post" REPLICA IDENTITY FULL;`; - await prisma.post.deleteMany(); - - const rawSub = await prisma.post.stream(); - - const enhanced = enhance(); - const enhancedSub = await enhanced.post.stream(); - - async function produce() { - await prisma.post.create({ data: { id: 1, name: 'abc' } }); - console.log('created'); - await prisma.post.update({ where: { id: 1 }, data: { name: 'bcd' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 1 } }); - console.log('deleted'); - - await prisma.post.create({ data: { id: 2, name: 'hello world' } }); - console.log('created'); - await prisma.post.update({ where: { id: 2 }, data: { name: 'hello moon' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 2 } }); - console.log('deleted'); - - await new Promise((resolve) => setTimeout(resolve, 3000)); - } - - const rawEvents: any[] = []; - const enhancedEvents: any[] = []; - await Promise.race([ - produce(), - consume(rawSub, 'Raw', rawEvents), - consume(enhancedSub, 'Enhanced', enhancedEvents), - ]); - expect(rawEvents.length).toBe(6); - expect(enhancedEvents.length).toBe(3); - }); - - it('should work with partial subscription', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - name String - - @@allow('read', contains(name, 'hello')) - } - `, - { - provider: 'postgresql', - dbUrl: PULSE_DB_URL, - pulseApiKey: PULSE_API_KEY, - logPrismaQuery: true, - } - ); - - await prisma.$queryRaw`ALTER TABLE public."Post" REPLICA IDENTITY FULL;`; - await prisma.post.deleteMany(); - - const rawSub = await prisma.post.stream({ create: {} }); - - const enhanced = enhance(); - const enhancedSub = await enhanced.post.stream({ create: {} }); - - async function produce() { - await prisma.post.create({ data: { id: 1, name: 'abc' } }); - console.log('created'); - await prisma.post.update({ where: { id: 1 }, data: { name: 'bcd' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 1 } }); - console.log('deleted'); - - await prisma.post.create({ data: { id: 2, name: 'hello world' } }); - console.log('created'); - await prisma.post.update({ where: { id: 2 }, data: { name: 'hello moon' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 2 } }); - console.log('deleted'); - - await new Promise((resolve) => setTimeout(resolve, 3000)); - } - - const rawEvents: any[] = []; - const enhancedEvents: any[] = []; - await Promise.race([ - produce(), - consume(rawSub, 'Raw', rawEvents), - consume(enhancedSub, 'Enhanced', enhancedEvents), - ]); - expect(rawEvents.length).toBe(2); - expect(enhancedEvents.length).toBe(1); - }); - - it('should work with combination of policies and user-provided filters', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - name String - - @@allow('read', contains(name, 'hello')) - } - `, - { - provider: 'postgresql', - dbUrl: PULSE_DB_URL, - pulseApiKey: PULSE_API_KEY, - logPrismaQuery: true, - } - ); - - await prisma.$queryRaw`ALTER TABLE public."Post" REPLICA IDENTITY FULL;`; - await prisma.post.deleteMany(); - - const rawSub = await prisma.post.stream({ - create: { name: { contains: 'world' } }, - update: { after: { name: { contains: 'world' } } }, - delete: { name: { contains: 'world' } }, - }); - - const enhanced = enhance(); - const enhancedSub = await enhanced.post.stream({ - create: { name: { contains: 'world' } }, - update: { after: { name: { contains: 'world' } } }, - delete: { name: { contains: 'world' } }, - }); - - async function produce() { - await prisma.post.create({ data: { id: 1, name: 'abc' } }); - console.log('created'); - await prisma.post.update({ where: { id: 1 }, data: { name: 'bcd' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 1 } }); - console.log('deleted'); - - await prisma.post.create({ data: { id: 2, name: 'good world' } }); - console.log('created'); - await prisma.post.update({ where: { id: 2 }, data: { name: 'nice world' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 2 } }); - console.log('deleted'); - - await prisma.post.create({ data: { id: 3, name: 'hello world' } }); - console.log('created'); - await prisma.post.update({ where: { id: 3 }, data: { name: 'hello nice world' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 3 } }); - console.log('deleted'); - - await new Promise((resolve) => setTimeout(resolve, 10000)); - } - - const rawEvents: any[] = []; - const enhancedEvents: any[] = []; - await Promise.race([ - produce(), - consume(rawSub, 'Raw', rawEvents), - consume(enhancedSub, 'Enhanced', enhancedEvents), - ]); - expect(rawEvents.length).toBe(6); - expect(enhancedEvents.length).toBe(3); - }); - - it('should work with field-level policies', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - name String @allow('read', contains(name, 'hello')) - - @@allow('all', true) - } - `, - { - provider: 'postgresql', - dbUrl: PULSE_DB_URL, - pulseApiKey: PULSE_API_KEY, - logPrismaQuery: true, - } - ); - - await prisma.$queryRaw`ALTER TABLE public."Post" REPLICA IDENTITY FULL;`; - await prisma.post.deleteMany(); - - const enhanced = enhance(); - const enhancedSub = await enhanced.post.stream(); - - async function produce() { - await prisma.post.create({ data: { id: 1, name: 'abc' } }); - console.log('created'); - await prisma.post.update({ where: { id: 1 }, data: { name: 'abc1' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 1 } }); - console.log('deleted'); - - await prisma.post.create({ data: { id: 2, name: 'hello1' } }); - console.log('created'); - await prisma.post.update({ where: { id: 2 }, data: { name: 'hello2' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 2 } }); - console.log('deleted'); - - await new Promise((resolve) => setTimeout(resolve, 10000)); - } - - const enhancedEvents: any[] = []; - await Promise.race([produce(), consume(enhancedSub, 'Enhanced', enhancedEvents)]); - expect(enhancedEvents.length).toBe(6); - expect(enhancedEvents.filter((e) => e.created?.name?.includes('abc'))).toHaveLength(0); - expect(enhancedEvents.filter((e) => e.updated?.before?.name?.includes('abc'))).toHaveLength(0); - expect(enhancedEvents.filter((e) => e.updated?.after?.name?.includes('abc'))).toHaveLength(0); - expect(enhancedEvents.filter((e) => e.updated?.name?.includes('abc'))).toHaveLength(0); - expect(enhancedEvents.filter((e) => e.deleted?.name?.includes('abc'))).toHaveLength(0); - }); - - it('should work with `@omit`', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - name String @omit - - @@allow('all', true) - } - `, - { - provider: 'postgresql', - dbUrl: PULSE_DB_URL, - pulseApiKey: PULSE_API_KEY, - logPrismaQuery: true, - } - ); - - await prisma.$queryRaw`ALTER TABLE public."Post" REPLICA IDENTITY FULL;`; - await prisma.post.deleteMany(); - - const enhanced = enhance(); - const enhancedSub = await enhanced.post.stream(); - - async function produce() { - await prisma.post.create({ data: { id: 1, name: 'abc' } }); - console.log('created'); - await prisma.post.update({ where: { id: 1 }, data: { name: 'bcd' } }); - console.log('updated'); - await prisma.post.delete({ where: { id: 1 } }); - console.log('deleted'); - - await new Promise((resolve) => setTimeout(resolve, 5000)); - } - - const enhancedEvents: any[] = []; - await Promise.race([produce(), consume(enhancedSub, 'Enhanced', enhancedEvents)]); - expect(enhancedEvents.length).toBe(3); - - for (const event of enhancedEvents) { - switch (event.action) { - case 'create': - expect(event.created.name).toBeUndefined(); - break; - case 'update': - expect(event.before?.name).toBeUndefined(); - expect(event.after?.name).toBeUndefined(); - break; - case 'delete': - expect(event.deleted.name).toBeUndefined(); - break; - } - } - }); -}); - -async function consume(subscription: any, name: string, events: any[]) { - console.log('Consuming', name); - for await (const event of subscription) { - console.log(name, 'got event:', event); - events.push(event); - } -} diff --git a/tests/integration/tests/enhancements/with-policy/query-reduction.test.ts b/tests/integration/tests/enhancements/with-policy/query-reduction.test.ts deleted file mode 100644 index 264119453..000000000 --- a/tests/integration/tests/enhancements/with-policy/query-reduction.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: query reduction', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('test query reduction', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - role String @default("User") - posts Post[] - private Boolean @default(false) - age Int - - @@allow('all', auth() == this) - @@allow('read', !private) - } - - model Post { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - title String - published Boolean @default(false) - viewCount Int @default(0) - - @@allow('all', auth() == user) - @@allow('read', published) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - role: 'User', - age: 18, - posts: { - create: [ - { id: 1, title: 'Post 1' }, - { id: 2, title: 'Post 2', published: true }, - ], - }, - }, - }); - await prisma.user.create({ - data: { - id: 2, - role: 'Admin', - age: 28, - private: true, - posts: { - create: [{ id: 3, title: 'Post 3', viewCount: 100 }], - }, - }, - }); - - const dbUser1 = enhance({ id: 1 }); - const dbUser2 = enhance({ id: 2 }); - - await expect( - dbUser1.user.findMany({ - where: { id: 2, AND: { age: { gt: 20 } } }, - }) - ).resolves.toHaveLength(0); - - await expect( - dbUser2.user.findMany({ - where: { id: 2, AND: { age: { gt: 20 } } }, - }) - ).resolves.toHaveLength(1); - - await expect( - dbUser1.user.findMany({ - where: { - AND: { age: { gt: 10 } }, - OR: [{ age: { gt: 25 } }, { age: { lt: 20 } }], - NOT: { private: true }, - }, - }) - ).resolves.toHaveLength(1); - - await expect( - dbUser2.user.findMany({ - where: { - AND: { age: { gt: 10 } }, - OR: [{ age: { gt: 25 } }, { age: { lt: 20 } }], - NOT: { private: true }, - }, - }) - ).resolves.toHaveLength(1); - - // to-many relation query - await expect( - dbUser1.user.findMany({ - where: { posts: { some: { published: true } } }, - }) - ).resolves.toHaveLength(1); - await expect( - dbUser1.user.findMany({ - where: { posts: { some: { AND: [{ published: true }, { viewCount: { gt: 0 } }] } } }, - }) - ).resolves.toHaveLength(0); - await expect( - dbUser2.user.findMany({ - where: { posts: { some: { AND: [{ published: false }, { viewCount: { gt: 0 } }] } } }, - }) - ).resolves.toHaveLength(1); - await expect( - dbUser1.user.findMany({ - where: { posts: { every: { published: true } } }, - }) - ).resolves.toHaveLength(0); - await expect( - dbUser1.user.findMany({ - where: { posts: { none: { published: true } } }, - }) - ).resolves.toHaveLength(0); - - // to-one relation query - await expect( - dbUser1.post.findMany({ - where: { user: { role: 'Admin' } }, - }) - ).resolves.toHaveLength(0); - await expect( - dbUser1.post.findMany({ - where: { user: { is: { role: 'Admin' } } }, - }) - ).resolves.toHaveLength(0); - await expect( - dbUser1.post.findMany({ - where: { user: { isNot: { role: 'User' } } }, - }) - ).resolves.toHaveLength(0); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/refactor.test.ts b/tests/integration/tests/enhancements/with-policy/refactor.test.ts deleted file mode 100644 index 6ee5c2343..000000000 --- a/tests/integration/tests/enhancements/with-policy/refactor.test.ts +++ /dev/null @@ -1,1166 +0,0 @@ -import { AuthUser, PrismaErrorCode } from '@zenstackhq/runtime'; -import { createPostgresDb, dropPostgresDb, loadSchemaFromFile, type FullDbClientContract } from '@zenstackhq/testtools'; -import path from 'path'; - -const DB_NAME = 'refactor'; - -describe('With Policy: refactor tests', () => { - let origDir: string; - let dbUrl: string; - let getDb: (user?: AuthUser) => FullDbClientContract; - let prisma: FullDbClientContract; - let anonDb: FullDbClientContract; - let adminDb: FullDbClientContract; - let user1Db: FullDbClientContract; - let user2Db: FullDbClientContract; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - beforeEach(async () => { - dbUrl = await createPostgresDb(DB_NAME); - - const { prisma: _prisma, enhance } = await loadSchemaFromFile( - path.join(__dirname, '../../schema/refactor-pg.zmodel'), - { - provider: 'postgresql', - dbUrl, - } - ); - getDb = enhance; - prisma = _prisma; - anonDb = getDb(); - user1Db = getDb({ id: 1 }); - user2Db = getDb({ id: 2 }); - adminDb = getDb({ id: 100, role: 'ADMIN' }); - }); - - afterEach(async () => { - process.chdir(origDir); - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb(DB_NAME); - }); - - it('read', async () => { - // empty table - await expect(anonDb.user.findMany()).resolves.toHaveLength(0); - await expect(anonDb.user.findUnique({ where: { id: 1 } })).toResolveNull(); - await expect(anonDb.user.findUniqueOrThrow({ where: { id: 1 } })).toBeNotFound(); - await expect(anonDb.user.findFirst({ where: { id: 1 } })).toResolveNull(); - await expect(anonDb.user.findFirstOrThrow({ where: { id: 1 } })).toBeNotFound(); - - await prisma.user.create({ - data: { - id: 1, - email: 'user1@zenstack.dev', - profile: { - create: { - name: 'User 1', - private: true, - }, - }, - posts: { - create: [ - { - title: 'Post 1', - published: true, - comments: { create: { id: 1, authorId: 1, content: 'Comment 1' } }, - }, - { - title: 'Post 2', - published: false, - comments: { create: { id: 2, authorId: 1, content: 'Comment 2' } }, - }, - ], - }, - }, - }); - - // simple read - await expect(anonDb.user.findMany()).resolves.toHaveLength(0); - await expect(adminDb.user.findMany()).resolves.toHaveLength(1); - await expect(user1Db.user.findMany()).resolves.toHaveLength(1); - await expect(user2Db.user.findMany()).resolves.toHaveLength(1); - await expect(anonDb.user.findUnique({ where: { id: 1 } })).toResolveNull(); - await expect(adminDb.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); - await expect(user1Db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); - await expect(user2Db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); - - // included profile got filtered - await expect(user1Db.user.findUnique({ include: { profile: true }, where: { id: 1 } })).resolves.toMatchObject({ - email: 'user1@zenstack.dev', - profile: expect.objectContaining({ name: 'User 1' }), - }); - await expect(user2Db.user.findUnique({ include: { profile: true }, where: { id: 1 } })).resolves.toMatchObject({ - email: 'user1@zenstack.dev', - profile: null, - }); - - // filter by profile - await expect(user1Db.user.findFirst({ where: { profile: { name: 'User 1' } } })).toResolveTruthy(); - await expect(user2Db.user.findFirst({ where: { profile: { name: 'User 1' } } })).toResolveFalsy(); - - // include profile cause toplevel user got filtered - await expect(user1Db.profile.findUnique({ include: { user: true }, where: { userId: 1 } })).toResolveTruthy(); - await expect(user2Db.profile.findUnique({ include: { user: true }, where: { userId: 1 } })).toResolveNull(); - - // posts got filtered - expect((await user1Db.user.findUnique({ include: { posts: true }, where: { id: 1 } })).posts).toHaveLength(2); - expect((await user2Db.user.findUnique({ include: { posts: true }, where: { id: 1 } })).posts).toHaveLength(1); - - // filter by posts - await expect( - user1Db.user.findFirst({ - where: { posts: { some: { title: 'Post 2' } } }, - }) - ).toResolveTruthy(); - await expect( - user2Db.user.findFirst({ - where: { posts: { some: { title: 'Post 2' } } }, - }) - ).toResolveFalsy(); - - // deep filter with comment - await expect( - user1Db.user.findFirst({ where: { posts: { some: { comments: { every: { content: 'Comment 2' } } } } } }) - ).toResolveTruthy(); - await expect( - user2Db.user.findFirst({ where: { posts: { some: { comments: { every: { content: 'Comment 2' } } } } } }) - ).toResolveNull(); - }); - - it('create', async () => { - // validation check - await expect( - anonDb.user.create({ - data: { email: 'abcd' }, - }) - ).toBeRejectedByPolicy(); - - // read back check - await expect( - anonDb.user.create({ - data: { id: 1, email: 'User1@zenstack.dev' }, - }) - ).rejects.toThrow(/not allowed to be read back/); - - // success - await expect(user1Db.user.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ - // email to lower - email: 'user1@zenstack.dev', - }); - - // nested creation failure - await expect( - anonDb.user.create({ - data: { - id: 2, - email: 'user2@zenstack.dev', - posts: { - create: { - id: 2, - title: 'A very long post title', - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - // check no partial creation - await expect(adminDb.user.findUnique({ where: { id: 2 } })).toResolveFalsy(); - - // deeply nested creation failure - await expect( - anonDb.user.create({ - data: { - id: 2, - email: 'user2@zenstack.dev', - posts: { - create: { - id: 2, - title: 'Post 2', - comments: { - create: { - authorId: 1, - content: 'Comment 2', - }, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - // check no partial creation - await expect(adminDb.user.findUnique({ where: { id: 2 } })).toResolveFalsy(); - - // deeply nested creation success - await expect( - user2Db.user.create({ - data: { - id: 2, - email: 'user2@zenstack.dev', - posts: { - create: { - id: 2, - title: ' Post 2 ', - published: true, - comments: { - create: { - authorId: 2, - content: 'Comment 2', - }, - }, - }, - }, - }, - include: { posts: true }, - }) - ).resolves.toMatchObject({ - posts: expect.arrayContaining([ - // title is trimmed - expect.objectContaining({ title: 'Post 2' }), - ]), - }); - - // create with connect: posts - await expect( - anonDb.user.create({ - data: { - id: 3, - email: 'user3@zenstack.dev', - posts: { - connect: { id: 3 }, - }, - }, - }) - ).toBeNotFound(); - await adminDb.post.create({ - data: { id: 3, authorId: 1, title: 'Post 3' }, - }); - await expect( - anonDb.user.create({ - data: { - id: 3, - email: 'user3@zenstack.dev', - posts: { - connect: { id: 3 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - anonDb.user.create({ - data: { - id: 3, - email: 'user3@zenstack.dev', - posts: { - connectOrCreate: { where: { id: 3 }, create: { title: 'Post 3' } }, - }, - }, - }) - ).toBeRejectedByPolicy(); - // success - await expect( - adminDb.user.create({ - data: { - id: 3, - email: 'user3@zenstack.dev', - posts: { - connect: { id: 3 }, - }, - }, - }) - ).toResolveTruthy(); - const r = await adminDb.user.create({ - include: { posts: true }, - data: { - id: 4, - email: 'user4@zenstack.dev', - posts: { - connectOrCreate: { where: { id: 4 }, create: { title: 'Post 4' } }, - }, - }, - }); - expect(r.posts[0].title).toEqual('Post 4'); - - // create with connect: profile - await expect( - anonDb.user.create({ - data: { - id: 5, - email: 'user5@zenstack.dev', - profile: { - connect: { id: 5 }, - }, - }, - }) - ).toBeNotFound(); - await adminDb.profile.create({ - data: { id: 5, userId: 1, name: 'User 5' }, - }); - await expect( - anonDb.user.create({ - data: { - id: 5, - email: 'user5@zenstack.dev', - profile: { - connect: { id: 5 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - anonDb.user.create({ - data: { - id: 5, - email: 'user5@zenstack.dev', - profile: { - connectOrCreate: { where: { id: 5 }, create: { name: 'User 5' } }, - }, - }, - }) - ).toBeRejectedByPolicy(); - // success - await expect( - adminDb.user.create({ - data: { - id: 5, - email: 'user5@zenstack.dev', - profile: { - connect: { id: 5 }, - }, - }, - }) - ).toResolveTruthy(); - const r1 = await adminDb.user.create({ - include: { profile: true }, - data: { - id: 6, - email: 'user6@zenstack.dev', - profile: { - connectOrCreate: { where: { id: 6 }, create: { name: 'User 6' } }, - }, - }, - }); - expect(r1.profile.name).toEqual('User 6'); - - // createMany, policy violation - await expect( - anonDb.user.create({ - data: { - id: 7, - email: 'user7@zenstack.dev', - posts: { - createMany: { - data: [ - { id: 7, title: 'Post 7.1' }, - { id: 8, title: 'Post 7.2 very long title' }, - ], - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - // no partial success - await expect(adminDb.user.findUnique({ where: { id: 7 } })).toResolveFalsy(); - - // createMany, unique constraint violation - await expect( - adminDb.user.create({ - data: { - id: 7, - email: 'user7@zenstack.dev', - posts: { - createMany: { - data: [ - { id: 7, title: 'Post 7.1' }, - { id: 7, title: 'Post 7.2' }, - ], - }, - }, - }, - }) - ).toBeRejectedWithCode(PrismaErrorCode.UNIQUE_CONSTRAINT_FAILED); - // no partial success - await expect(adminDb.user.findUnique({ where: { id: 7 } })).toResolveFalsy(); - - // createMany, skip duplicates - await expect( - adminDb.user.create({ - data: { - id: 7, - email: 'user7@zenstack.dev', - posts: { - createMany: { - data: [ - { id: 7, title: 'Post 7.1' }, - { id: 7, title: 'Post 7.2' }, - { id: 8, title: ' Post 8 ' }, - ], - skipDuplicates: true, - }, - }, - }, - }) - ).toResolveTruthy(); - // success - await expect(adminDb.user.findUnique({ where: { id: 7 } })).toResolveTruthy(); - await expect(adminDb.post.findUnique({ where: { id: 7 } })).toResolveTruthy(); - await expect(adminDb.post.findUnique({ where: { id: 8 } })).resolves.toMatchObject({ - // title is trimmed - title: 'Post 8', - }); - }); - - it('createMany', async () => { - await prisma.user.create({ - data: { id: 1, email: 'user1@zenstack.dev' }, - }); - - // success - await expect( - user1Db.post.createMany({ - data: [ - { id: 1, title: ' Post 1 ', authorId: 1 }, - { id: 2, title: 'Post 2', authorId: 1 }, - ], - }) - ).toResolveTruthy(); - - await expect(user1Db.post.findMany()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ title: 'Post 1' }), // title is trimmed - expect.objectContaining({ title: 'Post 2' }), - ]) - ); - - // unique constraint violation - await expect( - user1Db.post.createMany({ - data: [ - { id: 2, title: 'Post 2', authorId: 1 }, - { id: 3, title: 'Post 3', authorId: 1 }, - ], - }) - ).toBeRejectedWithCode(PrismaErrorCode.UNIQUE_CONSTRAINT_FAILED); - await expect(user1Db.post.findFirst({ where: { id: 3 } })).toResolveNull(); - - const r = await prisma.post.findMany(); - console.log('Existing:', JSON.stringify(r)); - - // ignore duplicates - await expect( - user1Db.post.createMany({ - data: [ - { id: 2, title: 'Post 2', authorId: 1 }, - { id: 3, title: 'Post 3', authorId: 1 }, - ], - skipDuplicates: true, - }) - ).resolves.toMatchObject({ count: 1 }); - await expect(user1Db.post.findFirst({ where: { id: 3 } })).toResolveTruthy(); - - // fail as a transaction - await expect( - user1Db.post.createMany({ - data: [ - { id: 4, title: 'Post 4 very very long', authorId: 1 }, - { id: 5, title: 'Post 5', authorId: 1 }, - ], - }) - ).toBeRejectedByPolicy(); - await expect(user1Db.post.findFirst({ where: { id: { in: [4, 5] } } })).toResolveNull(); - }); - - it('update single', async () => { - await prisma.user.create({ - data: { - id: 2, - email: 'user2@zenstack.dev', - }, - }); - await prisma.user.create({ - data: { - id: 1, - email: 'user1@zenstack.dev', - profile: { - create: { - id: 1, - name: 'User 1', - private: true, - }, - }, - posts: { - create: [ - { - id: 1, - title: 'Post 1', - published: true, - comments: { create: { authorId: 1, content: 'Comment 1' } }, - }, - { - id: 2, - title: 'Post 2', - published: false, - comments: { create: { authorId: 2, content: 'Comment 2' } }, - }, - ], - }, - }, - }); - - // top-level - await expect(anonDb.user.update({ where: { id: 3 }, data: { email: 'user2@zenstack.dev' } })).toBeNotFound(); - await expect( - anonDb.user.update({ where: { id: 1 }, data: { email: 'user2@zenstack.dev' } }) - ).toBeRejectedByPolicy(); - await expect( - user2Db.user.update({ where: { id: 1 }, data: { email: 'user2@zenstack.dev' } }) - ).toBeRejectedByPolicy(); - await expect( - adminDb.user.update({ where: { id: 1 }, data: { email: 'User1-nice@zenstack.dev' } }) - ).resolves.toMatchObject({ email: 'user1-nice@zenstack.dev' }); - - // update nested profile - await expect( - anonDb.user.update({ - where: { id: 1 }, - data: { profile: { update: { private: false } } }, - }) - ).toBeRejectedByPolicy(); - // variation: with where - await expect( - anonDb.user.update({ - where: { id: 1 }, - data: { profile: { update: { where: { private: true }, data: { private: false } } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - user2Db.user.update({ - where: { id: 1 }, - data: { profile: { update: { private: false } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { profile: { update: { private: false } } }, - }) - ).toResolveTruthy(); - // variation: with where - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { profile: { update: { where: { private: true }, data: { private: false } } } }, - }) - ).toBeNotFound(); - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { profile: { update: { where: { private: false }, data: { private: true } } } }, - }) - ).toResolveTruthy(); - - // update nested posts - await expect( - anonDb.user.update({ - where: { id: 1 }, - data: { posts: { update: { where: { id: 1 }, data: { published: false } } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - user2Db.user.update({ - where: { id: 1 }, - data: { posts: { update: { where: { id: 1 }, data: { published: false } } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { posts: { update: { where: { id: 1 }, data: { title: ' New ', published: false } } } }, - include: { posts: true }, - }) - ).resolves.toMatchObject({ posts: expect.arrayContaining([expect.objectContaining({ title: 'New' })]) }); - - // update nested comment prevent update of toplevel - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - email: 'user1-updated@zenstack.dev', - posts: { - update: { - where: { id: 2 }, - data: { - comments: { - update: { where: { id: 2 }, data: { content: 'Comment 2 updated' } }, - }, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect(adminDb.user.findUnique({ where: { email: 'user1-updated@zenstack.dev' } })).toResolveNull(); - await expect(adminDb.comment.findFirst({ where: { content: 'Comment 2 updated' } })).toResolveFalsy(); - - // update with create - const r1 = await user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - create: { - id: 3, - title: 'Post 3', - published: true, - comments: { - create: { author: { connect: { id: 1 } }, content: ' Comment 3 ' }, - }, - }, - }, - }, - include: { posts: { include: { comments: true } } }, - }); - expect(r1.posts[r1.posts.length - 1].comments[0].content).toEqual('Comment 3'); - - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - create: { - id: 4, - title: 'Post 4', - published: false, - comments: { - create: { - // can't create comment for unpublished post - author: { connect: { id: 1 } }, - content: 'Comment 4', - }, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect(user1Db.post.findUnique({ where: { id: 4 } })).toResolveNull(); - - // update with createMany - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - createMany: { - data: [ - { id: 4, title: ' Post 4 ' }, - { id: 5, title: 'Post 5' }, - ], - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(user1Db.post.findUnique({ where: { id: 4 } })).resolves.toMatchObject({ title: 'Post 4' }); - await expect( - user1Db.user.update({ - include: { posts: true }, - where: { id: 1 }, - data: { - posts: { - createMany: { - data: [ - { id: 5, title: 'Post 5' }, - { id: 6, title: 'Post 6' }, - ], - }, - }, - }, - }) - ).toBeRejectedWithCode(PrismaErrorCode.UNIQUE_CONSTRAINT_FAILED); - const r = await user1Db.user.update({ - include: { posts: true }, - where: { id: 1 }, - data: { - posts: { - createMany: { - data: [ - { id: 5, title: 'Post 5' }, - { id: 6, title: 'Post 6' }, - ], - skipDuplicates: true, - }, - }, - }, - }); - expect(r.posts).toHaveLength(6); - - // update with update - // profile - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - profile: { - update: { - name: 'User1 updated', - }, - }, - }, - }) - ).toResolveTruthy(); - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - profile: { - update: { - homepage: 'abc', // fail field validation - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - user2Db.user.update({ - where: { id: 1 }, - data: { - profile: { - update: { - name: 'User1 updated again', - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - // post - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - update: { - where: { id: 1 }, - data: { title: ' Post1-1' }, - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(user1Db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ title: 'Post1-1' }); - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - update: { - where: { id: 1 }, - data: { title: 'Post1 very long' }, // fail field validation - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - user2Db.user.update({ - where: { id: 1 }, - data: { - posts: { - update: { where: { id: 1 }, data: { title: 'Post1-2' } }, - }, - }, - }) - ).toBeRejectedByPolicy(); - // deep post - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - update: { - where: { id: 1 }, - data: { comments: { update: { where: { id: 1 }, data: { content: 'Comment1-1' } } } }, - }, - }, - }, - }) - ).toResolveTruthy(); - - // update with updateMany - // blocked by: https://github.com/prisma/prisma/issues/18371 - // await expect( - // user1Db.user.update({ - // where: { id: 1 }, - // data: { posts: { updateMany: { where: { id: { in: [1, 2, 3] } }, data: { title: 'My Post' } } } }, - // }) - // ).resolves.toMatchObject({ count: 3 }); - // await expect( - // user1Db.user.update({ - // where: { id: 1 }, - // data: { - // posts: { updateMany: { where: { id: { in: [1, 2, 3] } }, data: { title: 'Very long title' } } }, - // }, - // }) - // ).toBeRejectedByPolicy(); - // await expect( - // user2Db.user.update({ - // where: { id: 1 }, - // data: { posts: { updateMany: { where: { id: { in: [1, 2, 3] } }, data: { title: 'My Post' } } } }, - // }) - // ).toBeRejectedByPolicy(); - - // update with upsert - // post - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - upsert: { - where: { id: 1 }, - update: { title: ' Post 2' }, // update - create: { id: 7, title: 'Post 1' }, - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(user1Db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ title: 'Post 2' }); - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - upsert: { - where: { id: 7 }, - update: { title: 'Post 7-1' }, - create: { id: 7, title: ' Post 7' }, // create - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(user1Db.post.findUnique({ where: { id: 7 } })).resolves.toMatchObject({ title: 'Post 7' }); - await expect( - user2Db.user.update({ - where: { id: 1 }, - data: { - posts: { - upsert: { - where: { id: 7 }, - update: { title: 'Post 7-1' }, - create: { id: 1, title: 'Post 7' }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { - posts: { - upsert: { - where: { id: 7 }, - update: { title: 'Post 7 very long' }, - create: { title: 'Post 7' }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // update with connect - // post - await expect( - user1Db.user.update({ - where: { id: 2 }, - data: { - posts: { - connect: { id: 1 }, - }, - }, - }) - ).toResolveTruthy(); - await expect(adminDb.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ authorId: 2 }); - await expect( - user2Db.user.update({ - where: { id: 2 }, - data: { - posts: { - connect: { id: 2 }, // user2 can't update post2 - }, - }, - }) - ).toBeRejectedByPolicy(); - // profile - await expect( - user1Db.user.update({ where: { id: 2 }, data: { profile: { connect: { id: 1 } } } }) - ).toResolveTruthy(); - await expect(adminDb.profile.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ userId: 2 }); - await expect( - user1Db.user.update({ - where: { id: 1 }, - data: { profile: { connect: { id: 2 } } }, // user1 can't update profile1 - }) - ).toBeRejectedByPolicy(); - // reassign profile1 to user1 - await adminDb.user.update({ - where: { id: 1 }, - data: { profile: { connect: { id: 1 } } }, - }); - - // update with connectOrCreate - await expect( - user1Db.profile.update({ - where: { id: 1 }, - data: { - image: { - connectOrCreate: { - where: { id: 1 }, - create: { id: 1, url: 'abc' }, // validation error - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - user1Db.profile.update({ - where: { id: 1 }, - data: { - image: { - connectOrCreate: { - where: { id: 1 }, - create: { id: 1, url: 'http://abc.com/pic.png' }, // create - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(user1Db.image.findUnique({ where: { id: 1 } })).toResolveTruthy(); - await expect(user1Db.profile.findUnique({ include: { image: true }, where: { id: 1 } })).resolves.toMatchObject( - { id: 1 } - ); - await expect( - user1Db.profile.update({ - where: { id: 1 }, - data: { - image: { - connectOrCreate: { - where: { id: 1 }, - create: { id: 1, url: 'http://abc.com/pic1.png' }, // create - }, - }, - }, - }) - ).toResolveTruthy(); - await prisma.user.update({ - where: { id: 2 }, - data: { profile: { create: { id: 2, name: 'User 2' } } }, - }); - await prisma.image.create({ data: { id: 2, url: 'http://abc.com/pic2.png' } }); - await expect( - user1Db.profile.update({ - where: { id: 2 }, - data: { - image: { - // cause update to profile which is not allowed - connectOrCreate: { where: { id: 2 }, create: { id: 2, url: 'http://abc.com/pic2-1.png' } }, - }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - user2Db.profile.update({ - where: { id: 2 }, - data: { - image: { - connectOrCreate: { - where: { id: 2 }, // connect - create: { id: 2, url: 'http://abc.com/pic2-1.png' }, - }, - }, - }, - }) - ).toResolveTruthy(); - await expect(user2Db.profile.findUnique({ include: { image: true }, where: { id: 2 } })).resolves.toMatchObject( - { - image: { url: 'http://abc.com/pic2.png' }, - } - ); - - // update with disconnect - await expect( - user1Db.profile.update({ - where: { id: 2 }, - data: { image: { disconnect: true } }, - }) - ).toBeRejectedByPolicy(); - await expect( - user2Db.profile.update({ - where: { id: 2 }, - data: { image: { disconnect: true } }, - }) - ).toResolveTruthy(); - await expect(user2Db.profile.findUnique({ include: { image: true }, where: { id: 2 } })).resolves.toMatchObject( - { image: null } - ); - - // update with set - await prisma.image.create({ data: { id: 3, url: 'http://abc.com/pic3.png' } }); - await prisma.image.create({ data: { id: 4, url: 'http://abc.com/pic4.png' } }); - await prisma.image.create({ data: { id: 5, url: 'http://abc.com/pic5.png' } }); - await prisma.image.create({ data: { id: 6, url: 'http://abc.com/pic6.png' } }); - - await expect( - user1Db.comment.update({ - where: { id: 1 }, - data: { - images: { set: [{ id: 3 }, { id: 4 }] }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - adminDb.comment.update({ - where: { id: 1 }, - data: { - images: { set: [{ id: 3 }, { id: 4 }] }, - }, - }) - ).toResolveTruthy(); - await expect(adminDb.image.findUnique({ where: { id: 3 } })).resolves.toMatchObject({ commentId: 1 }); - await expect(adminDb.image.findUnique({ where: { id: 4 } })).resolves.toMatchObject({ commentId: 1 }); - await expect( - adminDb.comment.update({ - where: { id: 1 }, - data: { - images: { set: [{ id: 5 }, { id: 6 }] }, - }, - }) - ).toResolveTruthy(); - await expect(adminDb.image.findUnique({ where: { id: 3 } })).resolves.toMatchObject({ commentId: null }); - await expect(adminDb.image.findUnique({ where: { id: 4 } })).resolves.toMatchObject({ commentId: null }); - await expect(adminDb.image.findUnique({ where: { id: 5 } })).resolves.toMatchObject({ commentId: 1 }); - await expect(adminDb.image.findUnique({ where: { id: 6 } })).resolves.toMatchObject({ commentId: 1 }); - - // update with delete - await expect( - user1Db.comment.update({ - where: { id: 1 }, - data: { - images: { delete: [{ id: 5 }, { id: 6 }] }, - }, - }) - ).toBeRejectedByPolicy(); - await expect( - adminDb.comment.update({ - where: { id: 1 }, - data: { - images: { delete: [{ id: 5 }, { id: 6 }] }, - }, - }) - ).toResolveTruthy(); - await expect(adminDb.image.findUnique({ where: { id: 5 } })).toResolveNull(); - await expect(adminDb.image.findUnique({ where: { id: 6 } })).toResolveNull(); - - // update with deleteMany - await prisma.comment.update({ - where: { id: 1 }, - data: { - images: { set: [{ id: 3 }, { id: 4 }] }, - }, - }); - await expect( - user1Db.comment.update({ - where: { id: 1 }, - data: { images: { deleteMany: { url: { contains: 'pic3' } } } }, - }) - ).toBeRejectedByPolicy(); - await expect( - adminDb.comment.update({ - where: { id: 1 }, - data: { images: { deleteMany: { url: { contains: 'pic3' } } } }, - }) - ).toResolveTruthy(); - await expect(adminDb.image.findUnique({ where: { id: 3 } })).toResolveNull(); - }); - - it('updateMany', async () => { - await prisma.user.create({ - data: { - id: 1, - email: 'user1@zenstack.dev', - profile: { - create: { id: 1, name: 'User 1', private: true }, - }, - posts: { - create: [ - { id: 1, title: 'Post 1' }, - { id: 2, title: 'Post 2' }, - ], - }, - }, - }); - await expect( - user2Db.post.updateMany({ - data: { title: 'My post' }, - }) - ).resolves.toMatchObject({ count: 0 }); - await expect( - user1Db.post.updateMany({ - data: { title: 'My long long post' }, - }) - ).toBeRejectedByPolicy(); - await expect( - user1Db.post.updateMany({ - data: { title: ' My post' }, - }) - ).resolves.toMatchObject({ count: 2 }); - await expect(user1Db.post.findFirst()).resolves.toMatchObject({ title: 'My post' }); - }); - - it('delete single', async () => { - await prisma.user.create({ - data: { - id: 1, - email: 'user1@zenstack.dev', - profile: { - create: { id: 1, name: 'User 1', private: true }, - }, - posts: { - create: [ - { id: 1, title: 'Post 1', published: true }, - { id: 2, title: 'Post 2', published: false }, - ], - }, - }, - }); - - await expect(user2Db.post.delete({ where: { id: 1 } })).toBeRejectedByPolicy(); - await expect(user1Db.post.delete({ where: { id: 1 } })).toResolveTruthy(); - }); - - it('deleteMany', async () => { - await prisma.user.create({ - data: { - id: 1, - email: 'user1@zenstack.dev', - profile: { - create: { id: 1, name: 'User 1', private: true }, - }, - posts: { - create: [ - { id: 1, title: 'Post 1', published: true }, - { id: 2, title: 'Post 2', published: false }, - ], - }, - }, - }); - - await expect(user2Db.post.deleteMany({ where: { published: true } })).resolves.toMatchObject({ count: 0 }); - await expect(user1Db.post.deleteMany({ where: { published: true } })).resolves.toMatchObject({ count: 1 }); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/relation-check.test.ts b/tests/integration/tests/enhancements/with-policy/relation-check.test.ts deleted file mode 100644 index c08daa7d2..000000000 --- a/tests/integration/tests/enhancements/with-policy/relation-check.test.ts +++ /dev/null @@ -1,703 +0,0 @@ -import { loadModelWithError, loadSchema } from '@zenstackhq/testtools'; - -describe('Relation checker', () => { - it('should work for read', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('read', public) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('read', check(user, 'read')) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - public: true, - profile: { - create: { age: 18 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - public: false, - profile: { - create: { age: 20 }, - }, - }, - }); - - const db = enhance(); - await expect(db.profile.findMany()).resolves.toHaveLength(1); - }); - - it('should work for simple create', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('create', true) - @@allow('read', public) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('read', true) - @@allow('create', check(user, 'read')) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - public: true, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - public: false, - }, - }); - - const db = enhance(); - await expect(db.profile.create({ data: { user: { connect: { id: 1 } }, age: 18 } })).toResolveTruthy(); - await expect(db.profile.create({ data: { user: { connect: { id: 2 } }, age: 18 } })).toBeRejectedByPolicy(); - }); - - it('should work for nested create', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('create', true) - @@allow('read', public) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('read', true) - @@allow('create', age < 30 && check(user, 'read')) - } - ` - ); - - const db = enhance(); - - await expect( - db.user.create({ - data: { - id: 1, - public: true, - profile: { - create: { age: 18 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.user.create({ - data: { - id: 2, - public: false, - profile: { - create: { age: 18 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - id: 3, - public: true, - profile: { - create: { age: 30 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); - - it('should work for update', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('create', true) - @@allow('read', public) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('read', true) - @@allow('update', check(user, 'read') && age < 30) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - public: true, - profile: { - create: { id: 1, age: 18 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - public: false, - profile: { - create: { id: 2, age: 20 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 3, - public: true, - profile: { - create: { id: 3, age: 30 }, - }, - }, - }); - - const db = enhance(); - await expect(db.profile.update({ where: { id: 1 }, data: { age: 21 } })).toResolveTruthy(); - await expect(db.profile.update({ where: { id: 2 }, data: { age: 21 } })).toBeRejectedByPolicy(); - await expect(db.profile.update({ where: { id: 3 }, data: { age: 21 } })).toBeRejectedByPolicy(); - }); - - it('should work for delete', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('create', true) - @@allow('read', public) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('read', true) - @@allow('delete', check(user, 'read') && age < 30) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - public: true, - profile: { - create: { id: 1, age: 18 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - public: false, - profile: { - create: { id: 2, age: 20 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 3, - public: true, - profile: { - create: { id: 3, age: 30 }, - }, - }, - }); - - const db = enhance(); - await expect(db.profile.delete({ where: { id: 1 } })).toResolveTruthy(); - await expect(db.profile.delete({ where: { id: 2 } })).toBeRejectedByPolicy(); - await expect(db.profile.delete({ where: { id: 3 } })).toBeRejectedByPolicy(); - }); - - it('should work for field-level', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('read', public) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int @allow('read', age < 30 && check(user, 'read')) - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - public: true, - profile: { - create: { age: 18 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - public: false, - profile: { - create: { age: 20 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 3, - public: true, - profile: { - create: { age: 30 }, - }, - }, - }); - - const db = enhance(); - - const p1 = await db.profile.findUnique({ where: { id: 1 } }); - expect(p1.age).toBe(18); - const p2 = await db.profile.findUnique({ where: { id: 2 } }); - expect(p2.age).toBeUndefined(); - const p3 = await db.profile.findUnique({ where: { id: 3 } }); - expect(p3.age).toBeUndefined(); - }); - - it('should work for field-level with override', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('read', public) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int @allow('read', age < 30 && check(user, 'read'), true) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - public: true, - profile: { - create: { age: 18 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - public: false, - profile: { - create: { age: 20 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 3, - public: true, - profile: { - create: { age: 30 }, - }, - }, - }); - - const db = enhance(); - - const p1 = await db.profile.findUnique({ where: { id: 1 }, select: { age: true } }); - expect(p1.age).toBe(18); - const p2 = await db.profile.findUnique({ where: { id: 2 }, select: { age: true } }); - expect(p2).toBeNull(); - const p3 = await db.profile.findUnique({ where: { id: 3 }, select: { age: true } }); - expect(p3).toBeNull(); - }); - - it('should work for cross-model field comparison', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - age Int - @@allow('read', true) - @@allow('update', age == profile.age) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('read', true) - @@allow('update', check(user, 'update') && age < 30) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - age: 18, - profile: { - create: { id: 1, age: 18 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - age: 18, - profile: { - create: { id: 2, age: 20 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 3, - age: 30, - profile: { - create: { id: 3, age: 30 }, - }, - }, - }); - - const db = enhance(); - await expect(db.profile.update({ where: { id: 1 }, data: { age: 21 } })).toResolveTruthy(); - await expect(db.profile.update({ where: { id: 2 }, data: { age: 21 } })).toBeRejectedByPolicy(); - await expect(db.profile.update({ where: { id: 3 }, data: { age: 21 } })).toBeRejectedByPolicy(); - }); - - it('should work for implicit specific operations', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('read', public) - @@allow('create', true) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('read', check(user)) - @@allow('create', check(user)) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - public: true, - profile: { - create: { age: 18 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - public: false, - profile: { - create: { age: 20 }, - }, - }, - }); - - const db = enhance(); - await expect(db.profile.findMany()).resolves.toHaveLength(1); - - await prisma.user.create({ - data: { - id: 3, - public: true, - }, - }); - await expect(db.profile.create({ data: { user: { connect: { id: 3 } }, age: 18 } })).toResolveTruthy(); - - await prisma.user.create({ - data: { - id: 4, - public: false, - }, - }); - await expect(db.profile.create({ data: { user: { connect: { id: 4 } }, age: 18 } })).toBeRejectedByPolicy(); - }); - - it('should work for implicit all operations', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('all', public) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('all', check(user)) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - public: true, - profile: { - create: { age: 18 }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - public: false, - profile: { - create: { age: 20 }, - }, - }, - }); - - const db = enhance(); - await expect(db.profile.findMany()).resolves.toHaveLength(1); - - await prisma.user.create({ - data: { - id: 3, - public: true, - }, - }); - await expect(db.profile.create({ data: { user: { connect: { id: 3 } }, age: 18 } })).toResolveTruthy(); - - await prisma.user.create({ - data: { - id: 4, - public: false, - }, - }); - await expect(db.profile.create({ data: { user: { connect: { id: 4 } }, age: 18 } })).toBeRejectedByPolicy(); - }); - - it('should report error for invalid args', async () => { - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - public Boolean - @@allow('read', check(public)) - } - ` - ) - ).resolves.toContain('argument must be a relation field'); - - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - posts Post[] - @@allow('read', check(posts)) - } - model Post { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - } - ` - ) - ).resolves.toContain('argument cannot be an array field'); - - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - @@allow('read', check(profile.details)) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - details ProfileDetails? - } - - model ProfileDetails { - id Int @id @default(autoincrement()) - profile Profile @relation(fields: [profileId], references: [id]) - profileId Int - age Int - } - ` - ) - ).resolves.toContain('argument must be a relation field'); - - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - posts Post[] - @@allow('read', check(posts, 'all')) - } - model Post { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - } - ` - ) - ).resolves.toContain('argument must be a "read", "create", "update", or "delete"'); - }); - - it('should report error for cyclic relation check', async () => { - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - profileDetails ProfileDetails? - public Boolean - @@allow('all', check(profile)) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - details ProfileDetails? - @@allow('all', check(details)) - } - - model ProfileDetails { - id Int @id @default(autoincrement()) - profile Profile @relation(fields: [profileId], references: [id]) - profileId Int @unique - user User @relation(fields: [userId], references: [id]) - userId Int @unique - age Int - @@allow('all', check(user)) - } - ` - ) - ).resolves.toContain('cyclic dependency detected when following the `check()` call'); - }); - - it('should report error for cyclic relation check indirect', async () => { - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - public Boolean - @@allow('all', check(profile)) - } - - model Profile { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - details ProfileDetails? - @@allow('all', check(details)) - } - - model ProfileDetails { - id Int @id @default(autoincrement()) - profile Profile @relation(fields: [profileId], references: [id]) - profileId Int @unique - age Int - @@allow('all', check(profile)) - } - ` - ) - ).resolves.toContain('cyclic dependency detected when following the `check()` call'); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/relation-many-to-many-filter.test.ts b/tests/integration/tests/enhancements/with-policy/relation-many-to-many-filter.test.ts deleted file mode 100644 index e7ddb043e..000000000 --- a/tests/integration/tests/enhancements/with-policy/relation-many-to-many-filter.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: relation many-to-many filter', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - const model = ` - model M1 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m2 M2[] - - @@allow('read', !deleted) - @@allow('create', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m1 M1[] - - @@allow('read', !deleted) - @@allow('create', true) - } - `; - - it('some filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - value: 1, - m2: { - create: [ - { - id: '1', - value: 1, - }, - { - id: '2', - value: 2, - deleted: true, - }, - ], - }, - }, - }); - - // m1 -> m2 lookup - const r = await db.m1.findFirst({ - where: { - id: '1', - m2: { - some: {}, - }, - }, - include: { - _count: { select: { m2: true } }, - }, - }); - expect(r._count.m2).toBe(1); - - // m2 -> m1 lookup - await expect( - db.m2.findFirst({ - where: { - id: '1', - m1: { - some: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with empty m2 list - await db.m1.create({ - data: { - id: '2', - value: 1, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - some: {}, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - some: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - }); - - it('none filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - value: 1, - m2: { - create: [ - { id: '1', value: 1 }, - { id: '2', value: 2, deleted: true }, - ], - }, - }, - }); - - // m1 -> m2 lookup - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: {}, - }, - }, - }) - ).toResolveFalsy(); - - // m2 -> m1 lookup - await expect( - db.m2.findFirst({ - where: { - m1: { - none: {}, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with empty m2 list - await db.m1.create({ - data: { - id: '2', - value: 2, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - none: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - none: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('every filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - await db.m1.create({ - data: { - id: '1', - value: 1, - m2: { - create: [ - { id: '1', value: 1 }, - { id: '2', value: 2, deleted: true }, - ], - }, - }, - }); - - // m1 -> m2 lookup - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: {}, - }, - }, - }) - ).toResolveTruthy(); - - // m2 -> m1 lookup - await expect( - db.m2.findFirst({ - where: { - id: '1', - m1: { - every: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with empty m2 list - await db.m1.create({ - data: { - id: '2', - value: 2, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - every: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - every: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/relation-one-to-many-filter.test.ts b/tests/integration/tests/enhancements/with-policy/relation-one-to-many-filter.test.ts deleted file mode 100644 index 450726b87..000000000 --- a/tests/integration/tests/enhancements/with-policy/relation-one-to-many-filter.test.ts +++ /dev/null @@ -1,1026 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Relation one-to-many filter', () => { - const model = ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - m3 M3[] - - @@allow('read', !deleted) - @@allow('create', true) - } - - model M3 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id String - - @@allow('read', !deleted) - @@allow('create', true) - } - `; - - it('some filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - { - value: 2, - deleted: true, - m3: { - create: { - value: 2, - deleted: true, - }, - }, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - - // include clause - - const r = await db.m1.findFirst({ - where: { id: '1' }, - include: { - m2: { - where: { - m3: { - some: {}, - }, - }, - }, - }, - }); - expect(r.m2).toHaveLength(1); - - const r1 = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - some: { value: { gt: 1 } }, - }, - }, - }, - }, - }); - expect(r1.m2).toHaveLength(0); - - // m1 with empty m2 list - await db.m1.create({ - data: { - id: '2', - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - some: {}, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - some: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - }); - - it('none filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - { - value: 2, - deleted: true, - m3: { - create: { - value: 2, - deleted: true, - }, - }, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: {}, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveTruthy(); - - // include clause - - const r = await db.m1.findFirst({ - where: { id: '1' }, - include: { - m2: { - where: { - m3: { - none: {}, - }, - }, - }, - }, - }); - expect(r.m2).toHaveLength(0); - - const r1 = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - none: { value: { gt: 1 } }, - }, - }, - }, - }, - }); - expect(r1.m2).toHaveLength(1); - - // m1 with empty m2 list - await db.m1.create({ - data: { - id: '2', - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - none: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - none: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('every filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - { - value: 2, - deleted: true, - m3: { - create: { - value: 2, - deleted: true, - }, - }, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - - // include clause - - const r = await db.m1.findFirst({ - where: { id: '1' }, - include: { - m2: { - where: { - m3: { - every: {}, - }, - }, - }, - }, - }); - expect(r.m2).toHaveLength(1); - - const r1 = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - every: { value: { gt: 1 } }, - }, - }, - }, - }, - }); - expect(r1.m2).toHaveLength(0); - - // m1 with empty m2 list - await db.m1.create({ - data: { - id: '2', - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - every: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - every: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('_count filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - { - value: 2, - deleted: true, - m3: { - create: { - value: 2, - deleted: true, - }, - }, - }, - ], - }, - }, - }); - - await expect(db.m1.findFirst({ include: { _count: true } })).resolves.toMatchObject({ _count: { m2: 1 } }); - await expect(db.m1.findFirst({ include: { _count: { select: { m2: true } } } })).resolves.toMatchObject({ - _count: { m2: 1 }, - }); - await expect( - db.m1.findFirst({ include: { _count: { select: { m2: { where: { value: { gt: 0 } } } } } } }) - ).resolves.toMatchObject({ _count: { m2: 1 } }); - await expect( - db.m1.findFirst({ include: { _count: { select: { m2: { where: { value: { gt: 1 } } } } } } }) - ).resolves.toMatchObject({ _count: { m2: 0 } }); - - await expect(db.m1.findFirst({ include: { m2: { select: { _count: true } } } })).resolves.toMatchObject({ - m2: [{ _count: { m3: 1 } }], - }); - await expect( - db.m1.findFirst({ include: { m2: { select: { _count: { select: { m3: true } } } } } }) - ).resolves.toMatchObject({ m2: [{ _count: { m3: 1 } }] }); - await expect( - db.m1.findFirst({ - include: { m2: { select: { _count: { select: { m3: { where: { value: { gt: 1 } } } } } } } }, - }) - ).resolves.toMatchObject({ m2: [{ _count: { m3: 0 } }] }); - }); -}); - -describe('Relation one-to-many filter with field-level rules', () => { - const model = ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int @allow('read', !deleted) - deleted Boolean @default(false) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - m3 M3[] - - @@allow('read', true) - @@allow('create', true) - } - - model M3 { - id String @id @default(uuid()) - value Int @deny('read', deleted) - deleted Boolean @default(false) - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id String - - @@allow('read', true) - @@allow('create', true) - } - `; - - it('some filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - id: '2-1', - value: 1, - m3: { - create: { - id: '3-1', - value: 1, - }, - }, - }, - { - id: '2-2', - value: 2, - deleted: true, - m3: { - create: { - id: '3-2', - value: 2, - deleted: true, - }, - }, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: { id: '2-2' }, - }, - }, - }) - ).toResolveTruthy(); - - // include clause - - const r = await db.m1.findFirst({ - where: { id: '1' }, - include: { - m2: { - where: { - m3: { - some: {}, - }, - }, - }, - }, - }); - expect(r.m2).toHaveLength(2); - - let r1 = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - some: { value: { gt: 1 } }, - }, - }, - }, - }, - }); - expect(r1.m2).toHaveLength(0); - - r1 = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - some: { id: { equals: '3-2' } }, - }, - }, - }, - }, - }); - expect(r1.m2).toHaveLength(1); - }); - - it('none filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - id: '2-1', - value: 1, - m3: { - create: { - id: '3-1', - value: 1, - }, - }, - }, - { - id: '2-2', - value: 2, - deleted: true, - m3: { - create: { - id: '3-2', - value: 2, - deleted: true, - }, - }, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: {}, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: { id: '2-1' }, - }, - }, - }) - ).toResolveFalsy(); - - // include clause - - let r = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - none: { value: { gt: 1 } }, - }, - }, - }, - }, - }); - expect(r.m2).toHaveLength(2); - - r = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - none: { id: { equals: '3-2' } }, - }, - }, - }, - }, - }); - expect(r.m2).toHaveLength(1); - }); - - it('every filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - id: '2-1', - value: 1, - m3: { - create: { - id: '3-1', - value: 1, - }, - }, - }, - { - id: '2-2', - value: 2, - deleted: true, - m3: { - create: { - id: '3-2', - value: 2, - deleted: true, - }, - }, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: { id: { contains: '2' } }, - }, - }, - }) - ).toResolveTruthy(); - - // include clause - - const r = await db.m1.findFirst({ - where: { id: '1' }, - include: { - m2: { - where: { - m3: { - every: {}, - }, - }, - }, - }, - }); - expect(r.m2).toHaveLength(2); - - let r1 = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - every: { value: { gt: 1 } }, - }, - }, - }, - }, - }); - expect(r1.m2).toHaveLength(1); - - r1 = await db.m1.findFirst({ - where: { - id: '1', - }, - include: { - m2: { - where: { - m3: { - every: { id: { contains: '3' } }, - }, - }, - }, - }, - }); - expect(r1.m2).toHaveLength(2); - }); -}); - -describe('Relation one-to-many filter with field-level override rules', () => { - const model = ` - model M1 { - id String @id @default(uuid()) - m2 M2[] - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) @allow('read', true, true) - value Int @allow('read', !deleted) - deleted Boolean @default(false) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String - - @@allow('read', !deleted) - @@allow('create', true) - } - `; - - it('some filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - id: '2-1', - value: 1, - }, - { - id: '2-2', - value: 2, - deleted: true, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - some: { id: '2-2' }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('none filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - id: '2-1', - value: 1, - }, - { - id: '2-2', - value: 2, - deleted: true, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: {}, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - none: { id: '2-1' }, - }, - }, - }) - ).toResolveFalsy(); - }); - - it('every filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: [ - { - id: '2-1', - value: 1, - }, - { - id: '2-2', - value: 2, - deleted: true, - }, - ], - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: {}, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: { value: { gt: 1 } }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - every: { id: { contains: '2' } }, - }, - }, - }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/relation-one-to-one-filter.test.ts b/tests/integration/tests/enhancements/with-policy/relation-one-to-one-filter.test.ts deleted file mode 100644 index 1f8666fd5..000000000 --- a/tests/integration/tests/enhancements/with-policy/relation-one-to-one-filter.test.ts +++ /dev/null @@ -1,1111 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Relation one-to-one filter', () => { - const model = ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - m3 M3? - - @@allow('read', !deleted) - @@allow('create', true) - } - - model M3 { - id String @id @default(uuid()) - value Int - deleted Boolean @default(false) - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id String @unique - - @@allow('read', !deleted) - @@allow('create', true) - } - `; - - it('is filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - is: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - is: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '3', - m2: { - create: { - value: 1, - m3: { - create: { - value: 1, - deleted: true, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - is: { - m3: { value: 1 }, - }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with null m2 - await db.m1.create({ - data: { - id: '4', - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '4', - m2: { - is: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - }); - - it('isNot filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - isNot: { value: 0 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - isNot: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - isNot: { value: 0 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - isNot: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '3', - m2: { - create: { - value: 1, - m3: { - create: { - value: 1, - deleted: true, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - isNot: { - m3: { - isNot: { value: 0 }, - }, - }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - isNot: { - m3: { - isNot: { value: 1 }, - }, - }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with null m2 - await db.m1.create({ - data: { - id: '4', - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '4', - m2: { - isNot: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('direct object filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - value: 1, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - value: 1, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '3', - m2: { - create: { - value: 1, - m3: { - create: { - value: 1, - deleted: true, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - m3: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with null m2 - await db.m1.create({ - data: { - id: '4', - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '4', - m2: { - value: 1, - }, - }, - }) - ).toResolveFalsy(); - }); -}); - -describe('Relation one-to-one filter with field-level rules', () => { - const model = ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) - value Int @allow('read', !deleted) - deleted Boolean @default(false) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - m3 M3? - - @@allow('read', true) - @@allow('create', true) - } - - model M3 { - id String @id @default(uuid()) - value Int @allow('read', !deleted) - deleted Boolean @default(false) - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id String @unique - - @@allow('read', true) - @@allow('create', true) - } - `; - - it('is filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - id: '1', - value: 1, - m3: { - create: { - id: '1', - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - is: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - id: '2', - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - is: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - is: { id: '2' }, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '3', - m2: { - create: { - id: '3', - value: 1, - m3: { - create: { - id: '3', - value: 1, - deleted: true, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - is: { - m3: { value: 1 }, - }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - is: { - m3: { id: '3' }, - }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('isNot filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - id: '1', - value: 1, - m3: { - create: { - id: '1', - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - isNot: { value: 0 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - isNot: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - id: '2', - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - isNot: { value: 0 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - isNot: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - isNot: { id: '2' }, - }, - }, - }) - ).toResolveFalsy(); - }); - - it('direct object filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - id: '1', - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - value: 1, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - id: '2', - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - value: 1, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - id: '2', - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '3', - m2: { - create: { - id: '3', - value: 1, - m3: { - create: { - id: '3', - value: 1, - deleted: true, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - m3: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - m3: { id: '3' }, - }, - }, - }) - ).toResolveTruthy(); - }); -}); - -describe('Relation one-to-one filter with field-level override rules', () => { - const model = ` - model M1 { - id String @id @default(uuid()) - m2 M2? - - @@allow('all', true) - } - - model M2 { - id String @id @default(uuid()) @allow('read', true, true) - value Int - deleted Boolean @default(false) - m1 M1 @relation(fields: [m1Id], references:[id]) - m1Id String @unique - m3 M3? - - @@allow('read', !deleted) - @@allow('create', true) - } - - model M3 { - id String @id @default(uuid()) @allow('read', true, true) - value Int - deleted Boolean @default(false) - m2 M2 @relation(fields: [m2Id], references:[id]) - m2Id String @unique - - @@allow('read', !deleted) - @@allow('create', true) - } - `; - - it('is filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - id: '1', - value: 1, - m3: { - create: { - id: '1', - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - is: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - id: '2', - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - is: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - is: { id: '2' }, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '3', - m2: { - create: { - id: '3', - value: 1, - m3: { - create: { - id: '3', - value: 1, - deleted: true, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - is: { - m3: { value: 1 }, - }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - is: { - m3: { id: '3' }, - }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('isNot filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - id: '1', - value: 1, - m3: { - create: { - id: '1', - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - isNot: { value: 0 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - isNot: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - id: '2', - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - isNot: { value: 0 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - isNot: { value: 1 }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - isNot: { id: '2' }, - }, - }, - }) - ).toResolveFalsy(); - }); - - it('direct object filter', async () => { - const { enhance } = await loadSchema(model); - - const db = enhance(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '1', - m2: { - create: { - id: '1', - value: 1, - m3: { - create: { - value: 1, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '1', - m2: { - value: 1, - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 - await db.m1.create({ - data: { - id: '2', - m2: { - create: { - id: '2', - value: 1, - deleted: true, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - value: 1, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '2', - m2: { - id: '2', - }, - }, - }) - ).toResolveTruthy(); - - // m1 with m2 and m3 - await db.m1.create({ - data: { - id: '3', - m2: { - create: { - id: '3', - value: 1, - m3: { - create: { - id: '3', - value: 1, - deleted: true, - }, - }, - }, - }, - }, - }); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - m3: { value: 1 }, - }, - }, - }) - ).toResolveFalsy(); - - await expect( - db.m1.findFirst({ - where: { - id: '3', - m2: { - m3: { id: '3' }, - }, - }, - }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/self-relation.test.ts b/tests/integration/tests/enhancements/with-policy/self-relation.test.ts deleted file mode 100644 index 525d30043..000000000 --- a/tests/integration/tests/enhancements/with-policy/self-relation.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: self relations', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('one-to-one', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - value Int - successorId Int? @unique - successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id]) - predecessor User? @relation("BlogOwnerHistory") - - @@allow('create', value > 0) - @@allow('read', true) - } - ` - ); - - const db = enhance(); - - // create denied - await expect( - db.user.create({ - data: { - value: 0, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - successor: { - create: { - value: 0, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - successor: { - create: { - value: 1, - }, - }, - predecessor: { - create: { - value: 0, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - successor: { - create: { - value: 1, - }, - }, - predecessor: { - create: { - value: 1, - }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('one-to-many', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - value Int - teacherId Int? - teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id]) - students User[] @relation("TeacherStudents") - - @@allow('create', value > 0) - @@allow('read', true) - } - ` - ); - - const db = enhance(); - - // create denied - await expect( - db.user.create({ - data: { - value: 0, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - teacher: { - create: { value: 0 }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - teacher: { - create: { value: 1 }, - }, - students: { - create: [{ value: 0 }, { value: 1 }], - }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - teacher: { - create: { value: 1 }, - }, - students: { - create: [{ value: 1 }, { value: 2 }], - }, - }, - }) - ).toResolveTruthy(); - }); - - it('many-to-many', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - value Int - followedBy User[] @relation("UserFollows") - following User[] @relation("UserFollows") - - @@allow('create', value > 0) - @@allow('read', true) - } - ` - ); - - const db = enhance(); - - // create denied - await expect( - db.user.create({ - data: { - value: 0, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - followedBy: { create: { value: 0 } }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - followedBy: { create: { value: 1 } }, - following: { create: [{ value: 0 }, { value: 1 }] }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - db.user.create({ - data: { - value: 1, - followedBy: { create: { value: 1 } }, - following: { create: [{ value: 1 }, { value: 2 }] }, - }, - }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts b/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts deleted file mode 100644 index eb8c8e039..000000000 --- a/tests/integration/tests/enhancements/with-policy/todo-sample.test.ts +++ /dev/null @@ -1,512 +0,0 @@ -import { AuthUser } from '@zenstackhq/runtime'; -import { loadSchemaFromFile, run, type FullDbClientContract } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('Todo Policy Tests', () => { - let getDb: (user?: AuthUser) => FullDbClientContract; - let prisma: FullDbClientContract; - - beforeAll(async () => { - const { enhance, prisma: _prisma } = await loadSchemaFromFile( - path.join(__dirname, '../../schema/todo.zmodel'), - { addPrelude: false } - ); - getDb = enhance; - prisma = _prisma; - }); - - beforeEach(() => { - run('npx prisma migrate reset --force'); - run('npx prisma db push'); - }); - - it('user', async () => { - const user1 = { - id: 'user1', - email: 'user1@zenstack.dev', - name: 'User 1', - }; - const user2 = { - id: 'user2', - email: 'user2@zenstack.dev', - name: 'User 2', - }; - - const anonDb = getDb(); - const user1Db = getDb({ id: user1.id }); - const user2Db = getDb({ id: user2.id }); - - // create user1 - // create should succeed but result can be read back anonymously - 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(); - - // create user2 - await expect(anonDb.user.create({ data: user2 })).toBeRejectedByPolicy(); - - // find with user1 should only get user1 - const r = await user1Db.user.findMany(); - expect(r).toHaveLength(1); - expect(r[0]).toEqual(expect.objectContaining(user1)); - - // get user2 as user1 - await expect(user1Db.user.findUnique({ where: { id: user2.id } })).toResolveNull(); - - // add both users into the same space - await expect( - user1Db.space.create({ - data: { - name: 'Space 1', - slug: 'space1', - owner: { connect: { id: user1.id } }, - members: { - create: [ - { - user: { connect: { id: user1.id } }, - role: 'ADMIN', - }, - { - user: { connect: { id: user2.id } }, - role: 'USER', - }, - ], - }, - }, - }) - ).toResolveTruthy(); - - // now both user1 and user2 should be visible - await expect(user1Db.user.findMany()).resolves.toHaveLength(2); - await expect(user2Db.user.findMany()).resolves.toHaveLength(2); - - // update user2 as user1 - await expect( - user2Db.user.update({ - where: { id: user1.id }, - data: { name: 'hello' }, - }) - ).toBeRejectedByPolicy(); - - // update user1 as user1 - await expect( - user1Db.user.update({ - where: { id: user1.id }, - data: { name: 'hello' }, - }) - ).toResolveTruthy(); - - // delete user2 as user1 - await expect(user1Db.user.delete({ where: { id: user2.id } })).toBeRejectedByPolicy(); - - // delete user1 as user1 - await expect(user1Db.user.delete({ where: { id: user1.id } })).toResolveTruthy(); - await expect(user1Db.user.findUnique({ where: { id: user1.id } })).toResolveNull(); - }); - - it('todo list', async () => { - await createSpaceAndUsers(prisma); - - const anonDb = getDb(); - const emptyUIDDb = getDb({ id: '' }); - const user1Db = getDb({ id: user1.id }); - const user2Db = getDb({ id: user2.id }); - const user3Db = getDb({ id: user3.id }); - - await expect( - anonDb.list.create({ - data: { - id: 'list1', - title: 'List 1', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - await expect( - user1Db.list.create({ - data: { - id: 'list1', - title: 'List 1', - 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(); - - // accessible to owner - 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(); - - // inaccessible to user not in the space - await expect(user3Db.list.findUnique({ where: { id: 'list1' } })).toResolveNull(); - - // make a private list - await user1Db.list.create({ - data: { - id: 'list2', - title: 'List 2', - private: true, - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - // accessible to owner - 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(); - - // create a list which doesn't match credential should fail - await expect( - user1Db.list.create({ - data: { - id: 'list3', - title: 'List 3', - owner: { connect: { id: user2.id } }, - space: { connect: { id: space1.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - // create a list which doesn't match credential's space should fail - await expect( - user1Db.list.create({ - data: { - id: 'list3', - title: 'List 3', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space2.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - // update list - await expect( - user1Db.list.update({ - where: { id: 'list1' }, - data: { - title: 'List 1 updated', - }, - }) - ).resolves.toEqual(expect.objectContaining({ title: 'List 1 updated' })); - - await expect( - user2Db.list.update({ - where: { id: 'list1' }, - data: { - title: 'List 1 updated', - }, - }) - ).toBeRejectedByPolicy(); - - // delete list - await expect(user2Db.list.delete({ where: { id: 'list1' } })).toBeRejectedByPolicy(); - await expect(user1Db.list.delete({ where: { id: 'list1' } })).toResolveTruthy(); - await expect(user1Db.list.findUnique({ where: { id: 'list1' } })).toResolveNull(); - }); - - it('todo', async () => { - await createSpaceAndUsers(prisma); - - const user1Db = getDb({ id: user1.id }); - const user2Db = getDb({ id: user2.id }); - - // create a public list - await user1Db.list.create({ - data: { - id: 'list1', - title: 'List 1', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - // create - await expect( - user1Db.todo.create({ - data: { - id: 'todo1', - title: 'Todo 1', - owner: { connect: { id: user1.id } }, - list: { - connect: { id: 'list1' }, - }, - }, - }) - ).toResolveTruthy(); - - await expect( - user2Db.todo.create({ - data: { - id: 'todo2', - title: 'Todo 2', - owner: { connect: { id: user2.id } }, - list: { - connect: { id: 'list1' }, - }, - }, - }) - ).toResolveTruthy(); - - // read - await expect(user1Db.todo.findMany()).resolves.toHaveLength(2); - await expect(user2Db.todo.findMany()).resolves.toHaveLength(2); - - // update, user in the same space can freely update - await expect( - user1Db.todo.update({ - where: { id: 'todo1' }, - data: { - title: 'Todo 1 updated', - }, - }) - ).toResolveTruthy(); - await expect( - user1Db.todo.update({ - where: { id: 'todo2' }, - data: { - title: 'Todo 2 updated', - }, - }) - ).toResolveTruthy(); - - // create a private list - await user1Db.list.create({ - data: { - id: 'list2', - private: true, - title: 'List 2', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - // create - await expect( - user1Db.todo.create({ - data: { - id: 'todo3', - title: 'Todo 3', - owner: { connect: { id: user1.id } }, - list: { - connect: { id: 'list2' }, - }, - }, - }) - ).toResolveTruthy(); - - // reject because list2 is private - await expect( - user2Db.todo.create({ - data: { - id: 'todo4', - title: 'Todo 4', - owner: { connect: { id: user2.id } }, - list: { - connect: { id: 'list2' }, - }, - }, - }) - ).toBeRejectedByPolicy(); - - // update, only owner can update todo in a private list - await expect( - user1Db.todo.update({ - where: { id: 'todo3' }, - data: { - title: 'Todo 3 updated', - }, - }) - ).toResolveTruthy(); - await expect( - user2Db.todo.update({ - where: { id: 'todo3' }, - data: { - title: 'Todo 3 updated', - }, - }) - ).toBeRejectedByPolicy(); - }); - - it('relation query', async () => { - await createSpaceAndUsers(prisma); - - const user1Db = getDb({ id: user1.id }); - const user2Db = getDb({ id: user2.id }); - - await user1Db.list.create({ - data: { - id: 'list1', - title: 'List 1', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - await user1Db.list.create({ - data: { - id: 'list2', - title: 'List 2', - private: true, - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - }, - }); - - const r = await user1Db.space.findFirst({ - where: { id: 'space1' }, - include: { lists: true }, - }); - expect(r.lists).toHaveLength(2); - - const r1 = await user2Db.space.findFirst({ - where: { id: 'space1' }, - include: { lists: true }, - }); - expect(r1.lists).toHaveLength(1); - }); - - it('post-update checks', async () => { - await createSpaceAndUsers(prisma); - - const user1Db = getDb({ id: user1.id }); - - await user1Db.list.create({ - data: { - id: 'list1', - title: 'List 1', - owner: { connect: { id: user1.id } }, - space: { connect: { id: space1.id } }, - todos: { - create: { - id: 'todo1', - title: 'Todo 1', - owner: { connect: { id: user1.id } }, - }, - }, - }, - }); - - // change list's owner - await expect( - user1Db.list.update({ - where: { id: 'list1' }, - data: { - owner: { connect: { id: user2.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - // change todo's owner - await expect( - user1Db.todo.update({ - where: { id: 'todo1' }, - data: { - owner: { connect: { id: user2.id } }, - }, - }) - ).toBeRejectedByPolicy(); - - // nested change todo's owner - await expect( - user1Db.list.update({ - where: { id: 'list1' }, - data: { - todos: { - update: { - where: { id: 'todo1' }, - data: { - owner: { connect: { id: user2.id } }, - }, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); -}); - -const user1 = { - id: 'user1', - email: 'user1@zenstack.dev', - name: 'User 1', -}; - -const user2 = { - id: 'user2', - email: 'user2@zenstack.dev', - name: 'User 2', -}; - -const user3 = { - id: 'user3', - email: 'user3@zenstack.dev', - name: 'User 3', -}; - -const space1 = { - id: 'space1', - name: 'Space 1', - slug: 'space1', -}; - -const space2 = { - id: 'space2', - name: 'Space 2', - slug: 'space2', -}; - -async function createSpaceAndUsers(db: FullDbClientContract) { - // create users - await db.user.create({ data: user1 }); - await db.user.create({ data: user2 }); - await db.user.create({ data: user3 }); - - // add user1 and user2 into space1 - await db.space.create({ - data: { - ...space1, - members: { - create: [ - { - user: { connect: { id: user1.id } }, - role: 'ADMIN', - }, - { - user: { connect: { id: user2.id } }, - role: 'USER', - }, - ], - }, - }, - }); - - // add user3 to space2 - await db.space.create({ - data: { - ...space2, - members: { - create: [ - { - user: { connect: { id: user3.id } }, - role: 'ADMIN', - }, - ], - }, - }, - }); -} diff --git a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts deleted file mode 100644 index 3543dd7b5..000000000 --- a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: toplevel operations', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(async () => { - process.chdir(origDir); - }); - - it('read tests', async () => { - const { enhance, prisma } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('create', true) - @@allow('read', value > 1) - } - ` - ); - - const db = enhance(); - - await expect( - db.model.create({ - data: { - id: '1', - value: 1, - }, - }) - ).toBeRejectedByPolicy(); - const fromPrisma = await prisma.model.findUnique({ - where: { id: '1' }, - }); - expect(fromPrisma).toBeTruthy(); - - 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' } })).toBeNotFound(); - await expect(db.model.findFirstOrThrow({ where: { id: '1' } })).toBeNotFound(); - - const item2 = { - id: '2', - value: 2, - }; - const r1 = await db.model.create({ - data: item2, - }); - expect(r1).toBeTruthy(); - expect(await db.model.findMany()).toHaveLength(1); - expect(await db.model.findUnique({ where: { id: '2' } })).toEqual(expect.objectContaining(item2)); - expect(await db.model.findFirst({ where: { id: '2' } })).toEqual(expect.objectContaining(item2)); - expect(await db.model.findUniqueOrThrow({ where: { id: '2' } })).toEqual(expect.objectContaining(item2)); - expect(await db.model.findFirstOrThrow({ where: { id: '2' } })).toEqual(expect.objectContaining(item2)); - }); - - it('write tests', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('read', value > 1) - @@allow('create', value > 0) - @@allow('update', value > 1) - } - ` - ); - - const db = enhance(); - - // create denied - await expect( - db.model.create({ - data: { - value: 0, - }, - }) - ).toBeRejectedByPolicy(); - - // can't read back - await expect( - db.model.create({ - data: { - id: '1', - value: 1, - }, - }) - ).toBeRejectedByPolicy(); - - // success - expect( - await db.model.create({ - data: { - id: '2', - value: 2, - }, - }) - ).toBeTruthy(); - - // update not found - await expect(db.model.update({ where: { id: '3' }, data: { value: 5 } })).toBeNotFound(); - - // update-many empty - expect( - await db.model.updateMany({ - where: { id: '3' }, - data: { value: 5 }, - }) - ).toEqual(expect.objectContaining({ count: 0 })); - - // upsert - expect( - await db.model.upsert({ - where: { id: '3' }, - create: { id: '3', value: 5 }, - update: { value: 6 }, - }) - ).toEqual(expect.objectContaining({ value: 5 })); - - // update denied - await expect( - db.model.update({ - where: { id: '1' }, - data: { - value: 3, - }, - }) - ).toBeRejectedByPolicy(); - - // update success - expect( - await db.model.update({ - where: { id: '2' }, - data: { - value: 3, - }, - }) - ).toBeTruthy(); - }); - - it('update id tests', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('read', value > 1) - @@allow('create', value > 0) - @@allow('update', value > 1 && future().value > 2) - } - ` - ); - - const db = enhance(); - - await db.model.create({ - data: { - id: '1', - value: 2, - }, - }); - - // update denied - await expect( - db.model.update({ - where: { id: '1' }, - data: { - id: '2', - value: 1, - }, - }) - ).toBeRejectedByPolicy(); - - // update success - await expect( - db.model.update({ - where: { id: '1' }, - data: { - id: '2', - value: 3, - }, - }) - ).resolves.toMatchObject({ id: '2', value: 3 }); - - // upsert denied - await expect( - db.model.upsert({ - where: { id: '2' }, - update: { - id: '3', - value: 1, - }, - create: { - id: '4', - value: 5, - }, - }) - ).toBeRejectedByPolicy(); - - // upsert success - await expect( - db.model.upsert({ - where: { id: '2' }, - update: { - id: '3', - value: 4, - }, - create: { - id: '4', - value: 5, - }, - }) - ).resolves.toMatchObject({ id: '3', value: 4 }); - }); - - it('delete tests', async () => { - const { enhance, prisma } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - value Int - - @@allow('create', true) - @@allow('read', value > 2) - @@allow('delete', value > 1) - } - ` - ); - - const db = enhance(); - - await expect(db.model.delete({ where: { id: '1' } })).toBeNotFound(); - - await expect( - db.model.create({ - data: { id: '1', value: 1 }, - }) - ).toBeRejectedByPolicy(); - - await expect(db.model.delete({ where: { id: '1' } })).toBeRejectedByPolicy(); - expect(await prisma.model.findUnique({ where: { id: '1' } })).toBeTruthy(); - - await expect( - db.model.create({ - data: { id: '2', value: 2 }, - }) - ).toBeRejectedByPolicy(); - // deleted but unable to read back - await expect(db.model.delete({ where: { id: '2' } })).toBeRejectedByPolicy(); - expect(await prisma.model.findUnique({ where: { id: '2' } })).toBeNull(); - - await expect( - db.model.create({ - data: { id: '2', value: 2 }, - }) - ).toBeRejectedByPolicy(); - // only '2' is deleted, '1' is rejected by policy - expect(await db.model.deleteMany()).toEqual(expect.objectContaining({ count: 1 })); - expect(await prisma.model.findUnique({ where: { id: '2' } })).toBeNull(); - expect(await prisma.model.findUnique({ where: { id: '1' } })).toBeTruthy(); - - expect(await db.model.deleteMany()).toEqual(expect.objectContaining({ count: 0 })); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/unique-as-id.test.ts b/tests/integration/tests/enhancements/with-policy/unique-as-id.test.ts deleted file mode 100644 index 3f4de2fd1..000000000 --- a/tests/integration/tests/enhancements/with-policy/unique-as-id.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('With Policy: unique as id', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('unique fields', async () => { - const { prisma, enhance } = await loadSchema( - ` - model A { - x String @unique - y Int @unique - value Int - b B? - - @@allow('read', true) - @@allow('create', value > 0) - } - - model B { - b1 String @unique - b2 String @unique - value Int - a A @relation(fields: [ax], references: [x]) - ax String @unique - - @@allow('read', value > 2) - @@allow('create', value > 1) - } - ` - ); - - const db = enhance(); - - await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); - await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); - - await expect( - db.a.create({ data: { x: '2', y: 3, value: 1, b: { create: { b1: '1', b2: '2', value: 1 } } } }) - ).toBeRejectedByPolicy(); - - const r = await db.a.create({ - include: { b: true }, - data: { x: '2', y: 3, value: 1, b: { create: { b1: '1', b2: '2', value: 2 } } }, - }); - expect(r.b).toBeNull(); - const r1 = await prisma.b.findUnique({ where: { b1: '1' } }); - expect(r1.value).toBe(2); - - await expect( - db.a.create({ - include: { b: true }, - data: { x: '3', y: 4, value: 1, b: { create: { b1: '2', b2: '3', value: 3 } } }, - }) - ).toResolveTruthy(); - }); - - it('unique fields mixed with id', async () => { - const { prisma, enhance } = await loadSchema( - ` - model A { - id Int @id @default(autoincrement()) - x String @unique - y Int @unique - value Int - b B? - - @@allow('read', true) - @@allow('create', value > 0) - } - - model B { - id Int @id @default(autoincrement()) - b1 String @unique - b2 String @unique - value Int - a A @relation(fields: [ax], references: [x]) - ax String @unique - - @@allow('read', value > 2) - @@allow('create', value > 1) - } - ` - ); - - const db = enhance(); - - await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); - await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); - - await expect( - db.a.create({ data: { x: '2', y: 3, value: 1, b: { create: { b1: '1', b2: '2', value: 1 } } } }) - ).toBeRejectedByPolicy(); - - const r = await db.a.create({ - include: { b: true }, - data: { x: '2', y: 3, value: 1, b: { create: { b1: '1', b2: '2', value: 2 } } }, - }); - expect(r.b).toBeNull(); - const r1 = await prisma.b.findUnique({ where: { b1: '1' } }); - expect(r1.value).toBe(2); - - await expect( - db.a.create({ - include: { b: true }, - data: { x: '3', y: 4, value: 1, b: { create: { b1: '2', b2: '3', value: 3 } } }, - }) - ).toResolveTruthy(); - }); - - it('model-level unique fields', async () => { - const { prisma, enhance } = await loadSchema( - ` - model A { - x String - y Int - value Int - b B? - @@unique([x, y]) - - @@allow('read', true) - @@allow('create', value > 0) - } - - model B { - b1 String - b2 String - value Int - a A @relation(fields: [ax, ay], references: [x, y]) - ax String - ay Int - - @@allow('read', value > 2) - @@allow('create', value > 1) - - @@unique([ax, ay]) - @@unique([b1, b2]) - } - ` - ); - - const db = enhance(); - - await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); - await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); - - await expect( - db.a.create({ data: { x: '2', y: 1, value: 1, b: { create: { b1: '1', b2: '2', value: 1 } } } }) - ).toBeRejectedByPolicy(); - - const r = await db.a.create({ - include: { b: true }, - data: { x: '2', y: 1, value: 1, b: { create: { b1: '1', b2: '2', value: 2 } } }, - }); - expect(r.b).toBeNull(); - const r1 = await prisma.b.findUnique({ where: { b1_b2: { b1: '1', b2: '2' } } }); - expect(r1.value).toBe(2); - - await expect( - db.a.create({ - include: { b: true }, - data: { x: '3', y: 1, value: 1, b: { create: { b1: '2', b2: '2', value: 3 } } }, - }) - ).toResolveTruthy(); - }); - - it('unique fields with to-many nested update', async () => { - const { enhance } = await loadSchema( - ` - model A { - id Int @id @default(autoincrement()) - x Int - y Int - value Int - bs B[] - @@unique([x, y]) - - @@allow('read,create', true) - @@allow('update,delete', value > 0) - } - - model B { - id Int @id @default(autoincrement()) - value Int - a A @relation(fields: [aId], references: [id]) - aId Int - - @@allow('all', value > 0) - } - `, - { logPrismaQuery: true } - ); - - const db = enhance(); - - await db.a.create({ - data: { x: 1, y: 1, value: 1, bs: { create: [{ id: 1, value: 1 }] } }, - }); - - await db.a.create({ - data: { x: 2, y: 2, value: 2, bs: { create: [{ id: 2, value: 2 }] } }, - }); - - await db.a.update({ - where: { x_y: { x: 1, y: 1 } }, - data: { bs: { updateMany: { data: { value: 3 } } } }, - }); - - // check b#1 is updated - await expect(db.b.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ value: 3 }); - - // check b#2 is not affected - await expect(db.b.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ value: 2 }); - - await db.a.update({ - where: { x_y: { x: 1, y: 1 } }, - data: { bs: { deleteMany: {} } }, - }); - - // check b#1 is deleted - await expect(db.b.findUnique({ where: { id: 1 } })).resolves.toBeNull(); - - // check b#2 is not affected - await expect(db.b.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ value: 2 }); - }); - - it('unique fields with to-one nested update', async () => { - const { enhance } = await loadSchema( - ` - model A { - id Int @id @default(autoincrement()) - x Int - y Int - value Int - b B? - @@unique([x, y]) - - @@allow('read,create', true) - @@allow('update,delete', value > 0) - } - - model B { - id Int @id @default(autoincrement()) - value Int - a A @relation(fields: [aId], references: [id]) - aId Int @unique - - @@allow('all', value > 0) - } - ` - ); - - const db = enhance(); - - await db.a.create({ - data: { x: 1, y: 1, value: 1, b: { create: { id: 1, value: 1 } } }, - }); - - await db.a.create({ - data: { x: 2, y: 2, value: 2, b: { create: { id: 2, value: 2 } } }, - }); - - await db.a.update({ - where: { x_y: { x: 1, y: 1 } }, - data: { b: { update: { data: { value: 3 } } } }, - }); - - // check b#1 is updated - await expect(db.b.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ value: 3 }); - - // check b#2 is not affected - await expect(db.b.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ value: 2 }); - - await db.a.update({ - where: { x_y: { x: 1, y: 1 } }, - data: { b: { delete: true } }, - }); - - // check b#1 is deleted - await expect(db.b.findUnique({ where: { id: 1 } })).resolves.toBeNull(); - await expect(db.a.findUnique({ where: { x_y: { x: 1, y: 1 } }, include: { b: true } })).resolves.toMatchObject({ - b: null, - }); - - // check b#2 is not affected - await expect(db.b.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ value: 2 }); - await expect(db.a.findUnique({ where: { x_y: { x: 2, y: 2 } }, include: { b: true } })).resolves.toBeTruthy(); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/update-many-and-return.test.ts b/tests/integration/tests/enhancements/with-policy/update-many-and-return.test.ts deleted file mode 100644 index 4797cf670..000000000 --- a/tests/integration/tests/enhancements/with-policy/update-many-and-return.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Test API updateManyAndReturn', () => { - it('model-level policies', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - posts Post[] - level Int - - @@allow('read', level > 0) - } - - model Post { - id Int @id @default(autoincrement()) - title String - published Boolean @default(false) - userId Int - user User @relation(fields: [userId], references: [id]) - - @@allow('read', published) - @@allow('update', contains(title, 'hello')) - } - ` - ); - - await prisma.user.createMany({ - data: [{ id: 1, level: 1 }], - }); - await prisma.user.createMany({ - data: [{ id: 2, level: 0 }], - }); - - await prisma.post.createMany({ - data: [ - { id: 1, title: 'hello1', userId: 1, published: true }, - { id: 2, title: 'world1', userId: 1, published: false }, - ], - }); - - const db = enhance(); - - // only post#1 is updated - let r = await db.post.updateManyAndReturn({ - data: { title: 'foo' }, - }); - expect(r).toHaveLength(1); - expect(r[0].id).toBe(1); - - // post#2 is excluded from update - await expect( - db.post.updateManyAndReturn({ - where: { id: 2 }, - data: { title: 'foo' }, - }) - ).resolves.toHaveLength(0); - - // reset - await prisma.post.update({ where: { id: 1 }, data: { title: 'hello1' } }); - - // post#1 is updated - await expect( - db.post.updateManyAndReturn({ - where: { id: 1 }, - data: { title: 'foo' }, - }) - ).resolves.toHaveLength(1); - - // reset - await prisma.post.update({ where: { id: 1 }, data: { title: 'hello1' } }); - - // read-back check - // post#1 updated but can't be read back - await expect( - db.post.updateManyAndReturn({ - data: { published: false }, - }) - ).toBeRejectedByPolicy(['result is not allowed to be read back']); - // but the update should have been applied - await expect(prisma.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ published: false }); - - // reset - await prisma.post.update({ where: { id: 1 }, data: { published: true } }); - - // return relation - r = await db.post.updateManyAndReturn({ - include: { user: true }, - data: { title: 'hello2' }, - }); - expect(r[0]).toMatchObject({ user: { id: 1 } }); - - // relation filtered - await prisma.post.create({ data: { id: 3, title: 'hello3', userId: 2, published: true } }); - await expect( - db.post.updateManyAndReturn({ - where: { id: 3 }, - include: { user: true }, - data: { title: 'hello4' }, - }) - ).toBeRejectedByPolicy(['result is not allowed to be read back']); - // update is applied - await expect(prisma.post.findUnique({ where: { id: 3 } })).resolves.toMatchObject({ title: 'hello4' }); - }); - - it('field-level policies', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - title String @allow('read', published) - published Boolean @default(false) - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - // update should succeed but one result's title field can't be read back - await prisma.post.createMany({ - data: [ - { id: 1, title: 'post1', published: true }, - { id: 2, title: 'post2', published: false }, - ], - }); - - const r = await db.post.updateManyAndReturn({ - data: { title: 'foo' }, - }); - - expect(r.length).toBe(2); - expect(r[0].title).toBeTruthy(); - expect(r[1].title).toBeUndefined(); - - // check posts are updated - await expect(prisma.post.findMany({ where: { title: 'foo' } })).resolves.toHaveLength(2); - }); -}); diff --git a/tests/integration/tests/enhancements/with-policy/view.test.ts b/tests/integration/tests/enhancements/with-policy/view.test.ts deleted file mode 100644 index 82af3b9a5..000000000 --- a/tests/integration/tests/enhancements/with-policy/view.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -// TODO: revisit view after prisma 6.13 release -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('View Policy Test', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('view policy', async () => { - const { prisma, enhance } = await loadSchema( - ` - datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - - generator client { - provider = "prisma-client-js" - previewFeatures = ["views"] - } - - model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - posts Post[] - userInfo UserInfo? - } - - model Post { - id Int @id @default(autoincrement()) - title String - content String? - published Boolean @default(false) - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - } - - view UserInfo { - id Int @unique - name String - email String - postCount Int - user User @relation(fields: [id], references: [id]) - - @@allow('read', postCount > 1) - } - `, - { addPrelude: false } - ); - - await prisma.$executeRaw`CREATE VIEW UserInfo as select user.id, user.name, user.email, user.id as userId, count(post.id) as postCount from user left join post on user.id = post.authorId group by user.id;`; - - await prisma.user.create({ - data: { - email: 'alice@prisma.io', - name: 'Alice', - posts: { - create: { - title: 'Check out Prisma with Next.js', - content: 'https://www.prisma.io/nextjs', - published: true, - }, - }, - }, - }); - await prisma.user.create({ - data: { - email: 'bob@prisma.io', - name: 'Bob', - posts: { - create: [ - { - title: 'Follow Prisma on Twitter', - content: 'https://twitter.com/prisma', - published: true, - }, - { - title: 'Follow Nexus on Twitter', - content: 'https://twitter.com/nexusgql', - published: false, - }, - ], - }, - }, - }); - - const db = enhance(); - - await expect(prisma.userInfo.findMany()).resolves.toHaveLength(2); - await expect(db.userInfo.findMany()).resolves.toHaveLength(1); - - const r1 = await prisma.userInfo.findFirst({ include: { user: true } }); - expect(r1.user).toBeTruthy(); - - // user not readable - await expect(db.userInfo.findFirst({ include: { user: true } })).toResolveFalsy(); - }); -}); diff --git a/tests/integration/tests/frameworks/nextjs/generation.test.ts b/tests/integration/tests/frameworks/nextjs/generation.test.ts deleted file mode 100644 index 846634d94..000000000 --- a/tests/integration/tests/frameworks/nextjs/generation.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { run } from '@zenstackhq/testtools'; -import fs from 'fs'; -import fse from 'fs-extra'; -import path from 'path'; - -describe('SWR Hooks Generation Tests', () => { - let origDir: string; - - beforeAll(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('sqlite', async () => { - const testDir = path.join(__dirname, './test-run/sqlite'); - if (fs.existsSync(testDir)) { - fs.rmSync(testDir, { recursive: true, force: true }); - } - fs.mkdirSync(testDir, { recursive: true }); - fse.copySync(path.join(__dirname, './test-project'), testDir); - - process.chdir(testDir); - const nodePath = path.join(testDir, 'node_modules'); - run('npm install'); - run('npx zenstack generate --schema ./sqlite.zmodel', { NODE_PATH: nodePath }); - run('npm run build', { NODE_PATH: nodePath }); - }); - - it('postgres', async () => { - const testDir = path.join(__dirname, './test-run/postgres'); - if (fs.existsSync(testDir)) { - fs.rmSync(testDir, { recursive: true, force: true }); - } - fs.mkdirSync(testDir, { recursive: true }); - fse.copySync(path.join(__dirname, './test-project'), testDir); - - process.chdir(testDir); - const nodePath = path.join(testDir, 'node_modules'); - run('npm install'); - run('npx zenstack generate --schema ./postgres.zmodel', { NODE_PATH: nodePath }); - run('npm run build', { NODE_PATH: nodePath }); - }); -}); diff --git a/tests/integration/tests/frameworks/nextjs/test-project/.gitignore b/tests/integration/tests/frameworks/nextjs/test-project/.gitignore deleted file mode 100644 index c87c9b392..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/tests/integration/tests/frameworks/nextjs/test-project/.npmrc b/tests/integration/tests/frameworks/nextjs/test-project/.npmrc deleted file mode 100644 index e38856e8a..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/.npmrc +++ /dev/null @@ -1 +0,0 @@ -cache=../../../.npmcache diff --git a/tests/integration/tests/frameworks/nextjs/test-project/README.md b/tests/integration/tests/frameworks/nextjs/test-project/README.md deleted file mode 100644 index c87e0421d..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/README.md +++ /dev/null @@ -1,34 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/tests/integration/tests/frameworks/nextjs/test-project/next.config.js b/tests/integration/tests/frameworks/nextjs/test-project/next.config.js deleted file mode 100644 index ae887958d..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/next.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - swcMinify: true, -} - -module.exports = nextConfig diff --git a/tests/integration/tests/frameworks/nextjs/test-project/package.json b/tests/integration/tests/frameworks/nextjs/test-project/package.json deleted file mode 100644 index fbc7372ea..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "test-project", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@prisma/client": "6.19.x", - "@types/node": "18.11.18", - "@types/react": "18.0.27", - "@types/react-dom": "18.0.10", - "next": "14.2.4", - "react": "18.2.0", - "react-dom": "18.2.0", - "superjson": "^1.13.0", - "swr": "^2.2.0", - "typescript": "5.x", - "zod": "^3.25.0", - "@zenstackhq/language": "../../../../../../../packages/language/dist", - "@zenstackhq/runtime": "../../../../../../../packages/runtime/dist", - "@zenstackhq/sdk": "../../../../../../../packages/sdk/dist", - "@zenstackhq/swr": "../../../../../../../packages/plugins/swr/dist" - }, - "devDependencies": { - "prisma": "6.19.x" - } -} diff --git a/tests/integration/tests/frameworks/nextjs/test-project/pages/_app.tsx b/tests/integration/tests/frameworks/nextjs/test-project/pages/_app.tsx deleted file mode 100644 index 29ae42c3b..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/pages/_app.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { AppProps } from 'next/app'; - -export default function App({ Component, pageProps }: AppProps) { - return ; -} diff --git a/tests/integration/tests/frameworks/nextjs/test-project/pages/api/model/[...path].ts b/tests/integration/tests/frameworks/nextjs/test-project/pages/api/model/[...path].ts deleted file mode 100644 index ac3508248..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/pages/api/model/[...path].ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextRequestHandler } from '@zenstackhq/server/next'; -import { enhance } from '@zenstackhq/runtime'; -import { prisma } from '../../../server/db'; - -export default NextRequestHandler({ - getPrisma: (req, res) => enhance(prisma, { user: { id: 'user1' } }), -}); diff --git a/tests/integration/tests/frameworks/nextjs/test-project/pages/index.tsx b/tests/integration/tests/frameworks/nextjs/test-project/pages/index.tsx deleted file mode 100644 index 97eb48ff8..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/pages/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useFindManyPost } from '../lib/hooks'; - -export default function Home() { - const { data: posts } = useFindManyPost(); - return ( -
- {posts?.map((post) => ( -

{post.title}

- ))} -
- ); -} diff --git a/tests/integration/tests/frameworks/nextjs/test-project/postgres.zmodel b/tests/integration/tests/frameworks/nextjs/test-project/postgres.zmodel deleted file mode 100644 index 68edc64d1..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/postgres.zmodel +++ /dev/null @@ -1,31 +0,0 @@ -datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') -} - -generator js { - provider = 'prisma-client-js' -} - -plugin swr { - provider = '@zenstackhq/swr' - output = 'lib/hooks' -} - -model User { - id String @id - name String - posts Post[] - @@allow('create,read', true) - @@allow('update,delete', auth() == this) -} - -model Post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - @@allow('all', auth() == author) - @@allow('read', published) -} diff --git a/tests/integration/tests/frameworks/nextjs/test-project/server/db.ts b/tests/integration/tests/frameworks/nextjs/test-project/server/db.ts deleted file mode 100644 index c05a1eb79..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/server/db.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -declare global { - // eslint-disable-next-line no-var - var prisma: PrismaClient | undefined; -} - -export const prisma = global.prisma || new PrismaClient(); - -if (process.env.NODE_ENV !== 'production') { - global.prisma = prisma; -} diff --git a/tests/integration/tests/frameworks/nextjs/test-project/sqlite.zmodel b/tests/integration/tests/frameworks/nextjs/test-project/sqlite.zmodel deleted file mode 100644 index ff83da9f8..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/sqlite.zmodel +++ /dev/null @@ -1,31 +0,0 @@ -datasource db { - provider = 'sqlite' - url = 'file:./dev.db' -} - -generator js { - provider = 'prisma-client-js' -} - -plugin swr { - provider = '@zenstackhq/swr' - output = 'lib/hooks' -} - -model User { - id String @id - name String - posts Post[] - @@allow('create,read', true) - @@allow('update,delete', auth() == this) -} - -model Post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - @@allow('all', auth() == author) - @@allow('read', published) -} diff --git a/tests/integration/tests/frameworks/nextjs/test-project/tsconfig.json b/tests/integration/tests/frameworks/nextjs/test-project/tsconfig.json deleted file mode 100644 index 99710e857..000000000 --- a/tests/integration/tests/frameworks/nextjs/test-project/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} diff --git a/tests/integration/tests/frameworks/trpc/generation.test.ts b/tests/integration/tests/frameworks/trpc/generation.test.ts deleted file mode 100644 index 9c59ec7e3..000000000 --- a/tests/integration/tests/frameworks/trpc/generation.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { run } from '@zenstackhq/testtools'; -import fs from 'fs'; -import fse from 'fs-extra'; -import path from 'path'; - -describe('tRPC Routers Generation Tests', () => { - let origDir: string; - - beforeAll(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('basic', async () => { - const testDir = path.join(__dirname, './test-run/basic'); - if (fs.existsSync(testDir)) { - fs.rmSync(testDir, { recursive: true, force: true }); - } - fs.mkdirSync(testDir, { recursive: true }); - fse.copySync(path.join(__dirname, './test-project'), testDir); - - process.chdir(testDir); - run('npm install'); - run('npx zenstack generate --no-dependency-check --schema ./todo.zmodel', { - NODE_PATH: 'node_modules', - }); - run('npm run build', { NODE_PATH: 'node_modules' }); - }); -}); diff --git a/tests/integration/tests/frameworks/trpc/test-project/.gitignore b/tests/integration/tests/frameworks/trpc/test-project/.gitignore deleted file mode 100644 index c87c9b392..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/tests/integration/tests/frameworks/trpc/test-project/.npmrc b/tests/integration/tests/frameworks/trpc/test-project/.npmrc deleted file mode 100644 index e38856e8a..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/.npmrc +++ /dev/null @@ -1 +0,0 @@ -cache=../../../.npmcache diff --git a/tests/integration/tests/frameworks/trpc/test-project/README.md b/tests/integration/tests/frameworks/trpc/test-project/README.md deleted file mode 100644 index c87e0421d..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/README.md +++ /dev/null @@ -1,34 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/tests/integration/tests/frameworks/trpc/test-project/lib/trpc.ts b/tests/integration/tests/frameworks/trpc/test-project/lib/trpc.ts deleted file mode 100644 index 677b21cac..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/lib/trpc.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TRPCClientError, httpBatchLink } from '@trpc/client'; -import { createTRPCNext } from '@trpc/next'; -import superjson from 'superjson'; -import type { AppRouter } from '../server/routers/_app'; - -function getBaseUrl() { - if (typeof window !== 'undefined') return ''; - - if (process.env.VERCEL_URL) { - return `https://${process.env.VERCEL_URL}`; - } else { - return `http://localhost:${process.env.PORT ?? 3000}`; - } -} - -export const trpc = createTRPCNext({ - config({ ctx }) { - return { - transformer: superjson, - links: [ - httpBatchLink({ - url: `${getBaseUrl()}/api/trpc`, - }), - ], - }; - }, - ssr: false, -}); - -export function isTRPCClientError(error: unknown): error is TRPCClientError { - return error instanceof TRPCClientError; -} diff --git a/tests/integration/tests/frameworks/trpc/test-project/next.config.js b/tests/integration/tests/frameworks/trpc/test-project/next.config.js deleted file mode 100644 index ae887958d..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/next.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - swcMinify: true, -} - -module.exports = nextConfig diff --git a/tests/integration/tests/frameworks/trpc/test-project/package.json b/tests/integration/tests/frameworks/trpc/test-project/package.json deleted file mode 100644 index b39b03b87..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "test-project", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@prisma/client": "6.19.x", - "@tanstack/react-query": "^4.22.4", - "@trpc/client": "^10.34.0", - "@trpc/next": "^10.34.0", - "@trpc/react-query": "^10.34.0", - "@trpc/server": "^10.34.0", - "@types/node": "18.11.18", - "@types/react": "18.0.27", - "@types/react-dom": "18.0.10", - "next": "14.2.4", - "react": "18.2.0", - "react-dom": "18.2.0", - "superjson": "^1.13.0", - "typescript": "5.x", - "zod": "^3.25.0", - "@zenstackhq/language": "../../../../../../../packages/language/dist", - "@zenstackhq/runtime": "../../../../../../../packages/runtime/dist", - "@zenstackhq/sdk": "../../../../../../../packages/sdk/dist", - "@zenstackhq/server": "../../../../../../../packages/server/dist", - "@zenstackhq/trpc": "../../../../../../../packages/plugins/trpc/dist" - }, - "devDependencies": { - "prisma": "6.19.x" - } -} diff --git a/tests/integration/tests/frameworks/trpc/test-project/pages/_app.tsx b/tests/integration/tests/frameworks/trpc/test-project/pages/_app.tsx deleted file mode 100644 index b51367afa..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/pages/_app.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { AppProps } from 'next/app'; -import { trpc } from '../lib/trpc'; - -function App({ Component, pageProps }: AppProps) { - return ; -} - -export default trpc.withTRPC(App); diff --git a/tests/integration/tests/frameworks/trpc/test-project/pages/api/model/[...path].ts b/tests/integration/tests/frameworks/trpc/test-project/pages/api/model/[...path].ts deleted file mode 100644 index ac3508248..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/pages/api/model/[...path].ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextRequestHandler } from '@zenstackhq/server/next'; -import { enhance } from '@zenstackhq/runtime'; -import { prisma } from '../../../server/db'; - -export default NextRequestHandler({ - getPrisma: (req, res) => enhance(prisma, { user: { id: 'user1' } }), -}); diff --git a/tests/integration/tests/frameworks/trpc/test-project/pages/index.tsx b/tests/integration/tests/frameworks/trpc/test-project/pages/index.tsx deleted file mode 100644 index c9cddb4ef..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/pages/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { trpc } from '../lib/trpc'; - -export default function Home() { - const { data: posts } = trpc.post.findMany.useQuery({}); - return ( -
- {posts?.map((post) => ( -

{post.title}

- ))} -
- ); -} diff --git a/tests/integration/tests/frameworks/trpc/test-project/server/context.ts b/tests/integration/tests/frameworks/trpc/test-project/server/context.ts deleted file mode 100644 index 091001bed..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/server/context.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type inferAsyncReturnType } from '@trpc/server'; -import { type CreateNextContextOptions } from '@trpc/server/adapters/next'; -import { enhance } from '@zenstackhq/runtime'; -import { prisma } from './db'; - -export const createContext = async ({ req, res }: CreateNextContextOptions) => { - return { - prisma: enhance(prisma, { user: { id: 'user1' } }), - }; -}; - -export type Context = inferAsyncReturnType; diff --git a/tests/integration/tests/frameworks/trpc/test-project/server/db.ts b/tests/integration/tests/frameworks/trpc/test-project/server/db.ts deleted file mode 100644 index c05a1eb79..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/server/db.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -declare global { - // eslint-disable-next-line no-var - var prisma: PrismaClient | undefined; -} - -export const prisma = global.prisma || new PrismaClient(); - -if (process.env.NODE_ENV !== 'production') { - global.prisma = prisma; -} diff --git a/tests/integration/tests/frameworks/trpc/test-project/server/routers/_app.ts b/tests/integration/tests/frameworks/trpc/test-project/server/routers/_app.ts deleted file mode 100644 index 294095222..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/server/routers/_app.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createRouter } from './generated/routers'; -import { initTRPC } from '@trpc/server'; -import { type Context } from '../context'; -import superjson from 'superjson'; - -const t = initTRPC.context().create({ - transformer: superjson, -}); - -export const appRouter = createRouter(t.router, t.procedure); - -export type AppRouter = typeof appRouter; diff --git a/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel b/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel deleted file mode 100644 index 92363c825..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/todo.zmodel +++ /dev/null @@ -1,31 +0,0 @@ -datasource db { - provider = 'sqlite' - url = 'file:./dev.db' -} - -generator js { - provider = 'prisma-client-js' -} - -plugin trpc { - provider = '@zenstackhq/trpc' - output = 'server/routers/generated' -} - -model User { - id String @id - name String - posts Post[] - @@allow('create,read', true) - @@allow('update,delete', auth() == this) -} - -model Post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - published Boolean @default(false) - @@allow('all', auth() == author) - @@allow('read', published) -} diff --git a/tests/integration/tests/frameworks/trpc/test-project/tsconfig.json b/tests/integration/tests/frameworks/trpc/test-project/tsconfig.json deleted file mode 100644 index a020f8b91..000000000 --- a/tests/integration/tests/frameworks/trpc/test-project/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "paths": { - ".zenstack/zod/input": ["./node_modules/.zenstack/zod/input/index.d.ts"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} diff --git a/tests/integration/tests/misc/meta-annotation.test.ts b/tests/integration/tests/misc/meta-annotation.test.ts deleted file mode 100644 index 1c3988258..000000000 --- a/tests/integration/tests/misc/meta-annotation.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('`@@meta` attribute tests', () => { - it('generates generated into model-meta', async () => { - const { modelMeta } = await loadSchema( - ` - model User { - id Int @id - name String @meta('private', true) @meta('level', 1) - - @@meta(name: 'doc', value: 'It is user model') - @@meta('info', { description: 'This is a user model', geo: { country: 'US' } }) - } - ` - ); - - const model = modelMeta.models.user; - const userModelMeta = model.attributes.filter((attr: any) => attr.name === '@@meta'); - expect(userModelMeta).toEqual( - expect.arrayContaining([ - { - name: '@@meta', - args: expect.arrayContaining([ - { name: 'name', value: 'doc' }, - { name: 'value', value: 'It is user model' }, - ]), - }, - { - name: '@@meta', - args: expect.arrayContaining([ - { name: 'name', value: 'info' }, - { name: 'value', value: { description: 'This is a user model', geo: { country: 'US' } } }, - ]), - }, - ]) - ); - - const field = model.fields.name; - const fieldMeta = field.attributes.filter((attr: any) => attr.name === '@meta'); - expect(fieldMeta).toEqual( - expect.arrayContaining([ - { - name: '@meta', - args: expect.arrayContaining([ - { name: 'name', value: 'private' }, - { name: 'value', value: true }, - ]), - }, - { - name: '@meta', - args: expect.arrayContaining([ - { name: 'name', value: 'level' }, - { name: 'value', value: 1 }, - ]), - }, - ]) - ); - }); -}); diff --git a/tests/integration/tests/misc/prisma-client-generator.test.ts b/tests/integration/tests/misc/prisma-client-generator.test.ts deleted file mode 100644 index 4b0d48151..000000000 --- a/tests/integration/tests/misc/prisma-client-generator.test.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { createProjectAndCompile, loadSchema } from '@zenstackhq/testtools'; - -const PRISMA_CLIENT_JS_GENERATOR = ` -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -generator client { - provider = "prisma-client-js" -} -`; - -const PRISMA_CLIENT_GENERATOR = ` -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -generator client { - provider = "prisma-client" - output = "../generated/prisma" - moduleFormat = "cjs" -} -`; - -describe.each([ - { - isNewGenerator: false, - generator: PRISMA_CLIENT_JS_GENERATOR, - prismaLoadPath: '@prisma/client', - clientLoadPath: '@prisma/client', - modelsLoadPath: '@prisma/client', - enumsLoadPath: '@prisma/client', - enhanceLoadPath: '.zenstack/enhance', - output: undefined, - }, - { - isNewGenerator: true, - generator: PRISMA_CLIENT_GENERATOR, - prismaLoadPath: './generated/prisma/client', - clientLoadPath: './generated/zenstack/client', - modelsLoadPath: './generated/zenstack/models', - enumsLoadPath: './generated/zenstack/enums', - enhanceLoadPath: './generated/zenstack/enhance', - output: './generated/zenstack', - }, -])('Prisma-client generator tests', (config) => { - describe('regular client', () => { - it('exports expected modules', async () => { - await createProjectAndCompile( - ` - ${config.generator} - - enum Role { - USER - ADMIN - } - - model User { - id Int @id - name String - role Role - } - `, - { - addPrelude: false, - output: config.output, - prismaLoadPath: config.prismaLoadPath, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { Prisma, type User } from '${config.clientLoadPath}'; -${config.isNewGenerator ? "import type { UserModel } from '" + config.modelsLoadPath + "';" : ''} -import { Role } from '${config.enumsLoadPath}'; - -const user: User = { id: 1, name: 'Alice', role: Role.USER }; -console.log(user); - -${config.isNewGenerator ? "const user1: UserModel = { id: 1, name: 'Alice', role: 'USER' };\nconsole.log(user1);" : ''} - -const role = Role.USER; -console.log(role); - `, - }, - ], - } - ); - }); - - it('works with client extension', async () => { - await createProjectAndCompile( - ` - ${config.generator} - model User { - id Int @id @default(autoincrement()) - email String - } - `, - { - addPrelude: false, - output: config.output, - prismaLoadPath: config.prismaLoadPath, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { Prisma } from '${config.clientLoadPath}'; -import { PrismaClient } from '${config.prismaLoadPath}'; -import { enhance } from '${config.enhanceLoadPath}'; - -const prisma = new PrismaClient().$extends({ - model: { - user: { - async signUp(email: string) { - return prisma.user.create({ data: { email } }); - }, - }, - }, -}); - -async function main() { - const db = enhance(prisma); - const newUser = await db.user.signUp('user@test.com') -} - -main() - `, - }, - ], - } - ); - }); - }); - - describe('logical client', () => { - it('exports expected modules', async () => { - await createProjectAndCompile( - ` - ${config.generator} - - enum Role { - USER - ADMIN - } - - model User { - id Int @id - name String - role Role - } - - model Post { - id Int @id - authorId Int @default(auth().id) - } - `, - { - addPrelude: false, - output: config.output, - prismaLoadPath: config.prismaLoadPath, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { Prisma, type User } from '${config.clientLoadPath}'; -${config.isNewGenerator ? "import type { UserModel } from '" + config.modelsLoadPath + "';" : ''} -import { Role } from '${config.enumsLoadPath}'; - -const user: User = { id: 1, name: 'Alice', role: Role.USER }; -console.log(user); - -${config.isNewGenerator ? "const user1: UserModel = { id: 1, name: 'Alice', role: 'USER' };\nconsole.log(user1);" : ''} - -const role = Role.USER; -console.log(role); - `, - }, - ], - } - ); - }); - - it('works with client extension', async () => { - await createProjectAndCompile( - ` - ${config.generator} - model User { - id Int @id @default(autoincrement()) - email String - } - - model Post { - id Int @id @default(autoincrement()) - authorId Int @default(auth().id) - } - `, - { - addPrelude: false, - output: config.output, - prismaLoadPath: config.prismaLoadPath, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient } from '${config.prismaLoadPath}'; -import { enhance } from '${config.enhanceLoadPath}'; - -const prisma = new PrismaClient().$extends({ - model: { - user: { - async signUp(email: string) { - return prisma.user.create({ data: { email } }); - }, - }, - }, -}); - -async function main() { - const db = enhance(prisma); - const newUser = await db.user.signUp('user@test.com') -} - -main() - `, - }, - ], - } - ); - }); - - it('works with `auth` in `@default`', async () => { - const { enhance, prisma } = await loadSchema( - ` - ${config.generator} - - model User { - id Int @id - posts Post[] - @@allow('all', true) - } - - model Post { - id Int @id - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) - @@allow('all', true) - } - `, - { - addPrelude: false, - output: config.output, - compile: true, - prismaLoadPath: config.prismaLoadPath, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient } from '${config.prismaLoadPath}'; -import { enhance } from '${config.enhanceLoadPath}'; - -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - const post = await db.post.create({ data: { id: 1, title: 'Hello World' } }); - console.log(post.authorId); -} - -main(); -`, - }, - ], - } - ); - - const user = await prisma.user.create({ data: { id: 1 } }); - const db = enhance({ id: user.id }); - await expect(db.post.create({ data: { id: 1, title: 'Hello World' } })).resolves.toMatchObject({ - authorId: user.id, - }); - }); - - it('works with delegate models', async () => { - const { enhance } = await loadSchema( - ` - ${config.generator} - - model Asset { - id Int @id - name String - type String - @@delegate(type) - } - - model Post extends Asset { - title String - } - `, - { - enhancements: ['delegate'], - addPrelude: false, - output: config.output, - compile: true, - prismaLoadPath: config.prismaLoadPath, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient } from '${config.prismaLoadPath}'; -import { enhance } from '${config.enhanceLoadPath}'; - -const prisma = new PrismaClient(); -const db = enhance(prisma); - -async function main() { - const post = await db.post.create({ data: { id: 1, name: 'Test Post', title: 'Hello World' } }); - console.log(post.type, post.name, post.title); -} - -main(); -`, - }, - ], - } - ); - - const db = enhance(); - await expect( - db.post.create({ data: { id: 1, name: 'Test Post', title: 'Hello World' } }) - ).resolves.toMatchObject({ id: 1, name: 'Test Post', type: 'Post', title: 'Hello World' }); - }); - }); -}); diff --git a/tests/integration/tests/misc/stacktrace.test.ts b/tests/integration/tests/misc/stacktrace.test.ts deleted file mode 100644 index f652c5514..000000000 --- a/tests/integration/tests/misc/stacktrace.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('Stack trace tests', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('stack trace', async () => { - const { enhance } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - } - ` - ); - - const db = enhance(); - let error: Error | undefined = undefined; - - try { - await db.model.create({ data: {} }); - } catch (err) { - error = err as Error; - } - - expect(error?.stack).toContain( - "Error calling enhanced Prisma method `model.create`: denied by policy: model entities failed 'create' check" - ); - expect(error?.stack).toContain(`misc/stacktrace.test.ts`); - expect((error as any).internalStack).toBeTruthy(); - }); -}); diff --git a/tests/integration/tests/plugins/policy.test.ts b/tests/integration/tests/plugins/policy.test.ts deleted file mode 100644 index 3d9e75f98..000000000 --- a/tests/integration/tests/plugins/policy.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -/// - -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Policy plugin tests', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - const TRUE = { AND: [] }; - const FALSE = { OR: [] }; - - it('short-circuit', async () => { - const model = ` -model User { - id String @id @default(cuid()) - value Int -} - -model M { - id String @id @default(cuid()) - value Int - @@allow('read', auth() != null) - @@allow('create', auth().value > 0) - - @@allow('update', auth() != null) - @@deny('update', auth().value == null || auth().value <= 0) -} - `; - - const { policy } = await loadSchema(model); - - const m = policy.policy.m.modelLevel; - - expect((m.read.guard as Function)({ user: undefined })).toEqual(FALSE); - expect((m.read.guard as Function)({ user: { id: '1' } })).toEqual(TRUE); - - expect((m.create.guard as Function)({ user: undefined })).toEqual(FALSE); - expect((m.create.guard as Function)({ user: { id: '1' } })).toEqual(FALSE); - expect((m.create.guard as Function)({ user: { id: '1', value: 0 } })).toEqual(FALSE); - expect((m.create.guard as Function)({ user: { id: '1', value: 1 } })).toEqual(TRUE); - - expect((m.update.guard as Function)({ user: undefined })).toEqual(FALSE); - expect((m.update.guard as Function)({ user: { id: '1' } })).toEqual(FALSE); - expect((m.update.guard as Function)({ user: { id: '1', value: 0 } })).toEqual(FALSE); - expect((m.update.guard as Function)({ user: { id: '1', value: 1 } })).toEqual(TRUE); - }); - - it('no short-circuit', async () => { - const model = ` -model User { - id String @id @default(cuid()) - value Int -} - -model M { - id String @id @default(cuid()) - value Int - @@allow('read', auth() != null && value > 0) -} - `; - - const { policy } = await loadSchema(model); - - expect((policy.policy.m.modelLevel.read.guard as Function)({ user: undefined })).toEqual( - expect.objectContaining({ AND: [{ OR: [] }, { value: { gt: 0 } }] }) - ); - expect((policy.policy.m.modelLevel.read.guard as Function)({ user: { id: '1' } })).toEqual( - expect.objectContaining({ AND: [{ AND: [] }, { value: { gt: 0 } }] }) - ); - }); - - it('auth() multiple level member access', async () => { - const model = ` - model User { - id Int @id @default(autoincrement()) - cart Cart? - } - - model Cart { - id Int @id @default(autoincrement()) - tasks Task[] - user User @relation(fields: [userId], references: [id]) - userId Int @unique - } - - model Task { - id Int @id @default(autoincrement()) - cart Cart @relation(fields: [cartId], references: [id]) - cartId Int - value Int - @@allow('read', auth().cart.tasks?[id == 123] && value >10) - } - `; - - const { policy } = await loadSchema(model); - expect( - (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 1 }] } } }) - ).toEqual(expect.objectContaining({ AND: [{ OR: [] }, { value: { gt: 10 } }] })); - - expect( - (policy.policy.task.modelLevel.read.guard as Function)({ user: { cart: { tasks: [{ id: 123 }] } } }) - ).toEqual(expect.objectContaining({ AND: [{ AND: [] }, { value: { gt: 10 } }] })); - }); -}); diff --git a/tests/integration/tests/plugins/prisma.test.ts b/tests/integration/tests/plugins/prisma.test.ts deleted file mode 100644 index 32d5a8e59..000000000 --- a/tests/integration/tests/plugins/prisma.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import fs from 'fs'; -import path from 'path'; -import tmp from 'tmp'; - -tmp.setGracefulCleanup(); - -describe('Prisma plugin tests', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('standard output location', async () => { - const model = ` -model User { - id String @id @default(cuid()) -} - `; - const { projectDir } = await loadSchema(model); - expect(fs.existsSync(path.join(projectDir, './prisma/schema.prisma'))).toEqual(true); - }); - - it('relative output location', async () => { - const model = ` -model User { - id String @id @default(cuid()) -} - -plugin prisma { - provider = '@core/prisma' - output = './db/schema.prisma' -} - `; - const { projectDir } = await loadSchema(model, { pushDb: false }); - expect(fs.existsSync(path.join(projectDir, './db/schema.prisma'))).toEqual(true); - }); - - // eslint-disable-next-line jest/no-disabled-tests - it.skip('relative absolute location', async () => { - // disabling due to a possible bug in Prisma V5 - const { name: outDir } = tmp.dirSync({ unsafeCleanup: true }); - const model = ` -model User { - id String @id @default(cuid()) -} - -plugin prisma { - provider = '@core/prisma' - output = '${outDir}/db/schema.prisma' -} - `; - await loadSchema(model, { pushDb: false }); - expect(fs.existsSync(path.join(outDir, './db/schema.prisma'))).toEqual(true); - }); -}); diff --git a/tests/integration/tests/plugins/zod.test.ts b/tests/integration/tests/plugins/zod.test.ts deleted file mode 100644 index 154101c31..000000000 --- a/tests/integration/tests/plugins/zod.test.ts +++ /dev/null @@ -1,1124 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/// - -import { loadSchema } from '@zenstackhq/testtools'; -import { randomUUID } from 'crypto'; -import fs from 'fs'; -import path from 'path'; -import tmp from 'tmp'; - -describe('Zod plugin tests', () => { - let origDir: string; - - beforeEach(() => { - origDir = process.cwd(); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('basic generation', async () => { - const { zodSchemas } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - } - - enum Role { - USER - ADMIN - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email @endsWith('@zenstack.dev') - password String @omit - role Role @default(USER) - posts Post[] - age Int? - - @@validate(length(password, 6, 20)) - } - - model Post { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String @length(5, 10) - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - published Boolean @default(false) - viewCount Int @default(0) - viewMilliseconds BigInt @default(0) - } - `, - { addPrelude: false, pushDb: false } - ); - const schemas = zodSchemas.models; - expect(schemas.UserScalarSchema).toBeTruthy(); - expect(schemas.UserWithoutRefineSchema).toBeTruthy(); - expect(schemas.UserSchema).toBeTruthy(); - expect(schemas.UserCreateScalarSchema).toBeTruthy(); - expect(schemas.UserCreateWithoutRefineSchema).toBeTruthy(); - expect(schemas.UserCreateSchema).toBeTruthy(); - expect(schemas.UserUpdateScalarSchema).toBeTruthy(); - expect(schemas.UserUpdateWithoutRefineSchema).toBeTruthy(); - expect(schemas.UserUpdateSchema).toBeTruthy(); - expect(schemas.UserPrismaCreateSchema).toBeTruthy(); - expect(schemas.UserPrismaUpdateSchema).toBeTruthy(); - - // create - expect(schemas.UserCreateSchema.safeParse({ email: 'abc' }).success).toBeFalsy(); - expect(schemas.UserCreateSchema.safeParse({ role: 'Cook' }).success).toBeFalsy(); - expect(schemas.UserCreateSchema.safeParse({ email: 'abc@def.com' }).success).toBeFalsy(); - expect(schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev' }).success).toBeFalsy(); - expect( - schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', password: 'abc123' }).success - ).toBeTruthy(); - expect( - schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', role: 'ADMIN', password: 'abc' }).success - ).toBeFalsy(); - expect( - schemas.UserCreateWithoutRefineSchema.safeParse({ - email: 'abc@zenstack.dev', - role: 'ADMIN', - password: 'abc', - }).success - ).toBeTruthy(); - expect( - schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', role: 'ADMIN', password: 'abc123' }).success - ).toBeTruthy(); - - // create unchecked - expect( - zodSchemas.input.UserInputSchema.create.safeParse({ - data: { id: 1, email: 'abc@zenstack.dev', password: 'abc123' }, - }).success - ).toBeTruthy(); - - // update - expect(schemas.UserUpdateSchema.safeParse({}).success).toBeTruthy(); - expect(schemas.UserUpdateSchema.safeParse({ email: 'abc@def.com' }).success).toBeFalsy(); - expect(schemas.UserUpdateSchema.safeParse({ email: 'def@zenstack.dev' }).success).toBeTruthy(); - expect(schemas.UserUpdateSchema.safeParse({ password: 'pas' }).success).toBeFalsy(); - expect(schemas.UserUpdateWithoutRefineSchema.safeParse({ password: 'pas' }).success).toBeTruthy(); - expect(schemas.UserUpdateSchema.safeParse({ password: 'password456' }).success).toBeTruthy(); - - // update unchecked - expect( - zodSchemas.input.UserInputSchema.update.safeParse({ where: { id: 1 }, data: { id: 2 } }).success - ).toBeTruthy(); - - // model schema - - // missing fields - expect( - schemas.UserSchema.safeParse({ - id: 1, - email: 'abc@zenstack.dev', - }).success - ).toBeFalsy(); - - expect( - schemas.UserSchema.safeParse({ - id: 1, - createdAt: new Date(), - updatedAt: new Date(), - email: 'abc@zenstack.dev', - role: 'ADMIN', - }).success - ).toBeTruthy(); - - // without omitted field - expect( - schemas.UserSchema.safeParse({ - id: 1, - email: 'abc@zenstack.dev', - role: 'ADMIN', - createdAt: new Date(), - updatedAt: new Date(), - }).success - ).toBeTruthy(); - - // with optional field - expect( - schemas.UserSchema.safeParse({ - id: 1, - email: 'abc@zenstack.dev', - role: 'ADMIN', - createdAt: new Date(), - updatedAt: new Date(), - age: 18, - }).success - ).toBeTruthy(); - - // with omitted field - const withPwd = schemas.UserSchema.safeParse({ - id: 1, - email: 'abc@zenstack.dev', - role: 'ADMIN', - createdAt: new Date(), - updatedAt: new Date(), - password: 'abc123', - }); - expect(withPwd.success).toBeTruthy(); - expect(withPwd.data.password).toBeUndefined(); - - expect(schemas.PostSchema).toBeTruthy(); - expect(schemas.PostCreateSchema).toBeTruthy(); - expect(schemas.PostUpdateSchema).toBeTruthy(); - expect(schemas.PostCreateSchema.safeParse({ title: 'abc' }).success).toBeFalsy(); - expect(schemas.PostCreateSchema.safeParse({ title: 'abcabcabcabc' }).success).toBeFalsy(); - expect(schemas.PostCreateSchema.safeParse({ title: 'abcde' }).success).toBeTruthy(); - schemas.PostCreateSchema.parse({ title: 'abcde', authorId: 1 }); - expect(schemas.PostCreateSchema.safeParse({ title: 'abcde', authorId: 1 }).data.authorId).toBe(1); - expect(schemas.PostUpdateSchema.safeParse({ authorId: 1 }).data.authorId).toBe(1); - - expect(schemas.PostPrismaCreateSchema.safeParse({ title: 'a' }).success).toBeFalsy(); - expect(schemas.PostPrismaCreateSchema.safeParse({ title: 'abcde' }).success).toBeTruthy(); - expect(schemas.PostPrismaCreateSchema.safeParse({ viewCount: 1 }).success).toBeTruthy(); - expect(schemas.PostPrismaCreateSchema.safeParse({ viewMilliseconds: 1n }).success).toBeTruthy(); - - expect(schemas.PostPrismaUpdateSchema.safeParse({ title: 'a' }).success).toBeFalsy(); - expect(schemas.PostPrismaUpdateSchema.safeParse({ title: 'abcde' }).success).toBeTruthy(); - expect(schemas.PostPrismaUpdateSchema.safeParse({ viewCount: 1 }).success).toBeTruthy(); - expect(schemas.PostPrismaUpdateSchema.safeParse({ viewCount: { increment: 1 } }).success).toBeTruthy(); - expect(schemas.PostPrismaUpdateSchema.safeParse({ viewMilliseconds: 1n }).success).toBeTruthy(); - }); - - it('mixed casing', async () => { - const model = ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - } - - enum role { - USER - ADMIN - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email @endsWith('@zenstack.dev') - password String @omit - role role @default(USER) - posts post_item[] - profile userProfile? - } - - model userProfile { - id Int @id @default(autoincrement()) - bio String - user User @relation(fields: [userId], references: [id]) - userId Int @unique - } - - model post_item { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String @length(5, 10) - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - published Boolean @default(false) - viewCount Int @default(0) - reviews review[] - } - - model review { - id Int @id @default(autoincrement()) - post post_item @relation(fields: [postId], references: [id]) - postId Int @unique - } - `; - - await loadSchema(model, { addPrelude: false, pushDb: false }); - }); - - it('attribute coverage', async () => { - const model = ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - } - - model M { - id Int @id @default(autoincrement()) - a String? @length(5, 10, 'must be between 5 and 10') - b String? @length(5) - c String? @length(min: 5, message: 'must be at least 5') - d String? @length(max: 10) - e String? @length(min: 5, max: 10, 'must be between 5 and 10') - f String? @startsWith('abc', 'must start with abc') - g String? @endsWith('abc', 'must end with abc') - h String? @contains('abc', 'must contain abc') - i String? @regex('^abc$', 'must match /^abc$/') - j String? @email('must be an email') - k String? @url('must be a url') - l String? @datetime('must be a datetime') - m Int? @gt(1, 'must be greater than 1') - n Int? @gte(1, 'must be greater than or equal to 1') - o Int? @lt(1, 'must be less than 1') - p Int? @lte(1, 'must be less than or equal to 1') - q Int[] - r String? @db.Uuid - } - `; - - const { zodSchemas } = await loadSchema(model, { addPrelude: false, pushDb: false }); - - const schema = zodSchemas.models.MCreateSchema; - - expect(schema.safeParse({ a: 'abc' }).error.toString()).toMatch(/must be between 5 and 10/); - expect(schema.safeParse({ a: 'abcabcabcabc' }).error.toString()).toMatch(/must be between 5 and 10/); - expect(schema.safeParse({ a: 'abcde' }).success).toBeTruthy(); - - expect(schema.safeParse({ b: 'abc' }).success).toBeFalsy(); - expect(schema.safeParse({ b: 'abcde' }).success).toBeTruthy(); - - expect(schema.safeParse({ c: 'abc' }).error.toString()).toMatch(/must be at least 5/); - expect(schema.safeParse({ c: 'abcdef' }).success).toBeTruthy(); - - expect(schema.safeParse({ d: 'abcabcabcabc' }).success).toBeFalsy(); - expect(schema.safeParse({ d: 'abcdef' }).success).toBeTruthy(); - - expect(schema.safeParse({ e: 'abc' }).error.toString()).toMatch(/must be between 5 and 10/); - expect(schema.safeParse({ e: 'abcabcabcabc' }).error.toString()).toMatch(/must be between 5 and 10/); - expect(schema.safeParse({ e: 'abcde' }).success).toBeTruthy(); - - expect(schema.safeParse({ f: 'abcde' }).success).toBeTruthy(); - expect(schema.safeParse({ f: '1abcde' }).error.toString()).toMatch(/must start with abc/); - - expect(schema.safeParse({ g: '123abc' }).success).toBeTruthy(); - expect(schema.safeParse({ g: '123abcd' }).error.toString()).toMatch(/must end with abc/); - - expect(schema.safeParse({ h: '123abcdef' }).success).toBeTruthy(); - expect(schema.safeParse({ h: '123abdef' }).error.toString()).toMatch(/must contain abc/); - - expect(schema.safeParse({ i: 'abc' }).success).toBeTruthy(); - expect(schema.safeParse({ i: '1abc' }).error.toString()).toMatch(/must match \/\^abc\$/); - - expect(schema.safeParse({ j: 'abc@zenstack.dev' }).success).toBeTruthy(); - expect(schema.safeParse({ j: 'abc@haha' }).error.toString()).toMatch(/must be an email/); - - expect(schema.safeParse({ k: 'https://zenstack.dev' }).success).toBeTruthy(); - expect(schema.safeParse({ k: 'abc123' }).error.toString()).toMatch(/must be a url/); - - expect(schema.safeParse({ l: '2020-01-01T00:00:00Z' }).success).toBeTruthy(); - expect(schema.safeParse({ l: '2020-01-01T00:00:00+02:00' }).success).toBeTruthy(); - expect(schema.safeParse({ l: '2022-01' }).error.toString()).toMatch(/must be a datetime/); - - expect(schema.safeParse({ m: 2 }).success).toBeTruthy(); - expect(schema.safeParse({ m: 1 }).error.toString()).toMatch(/must be greater than 1/); - - expect(schema.safeParse({ n: 1 }).success).toBeTruthy(); - expect(schema.safeParse({ n: 0 }).error.toString()).toMatch(/must be greater than or equal to 1/); - - expect(schema.safeParse({ o: 0 }).success).toBeTruthy(); - expect(schema.safeParse({ o: 1 }).error.toString()).toMatch(/must be less than 1/); - - expect(schema.safeParse({ p: 1 }).success).toBeTruthy(); - expect(schema.safeParse({ p: 2 }).error.toString()).toMatch(/must be less than or equal to 1/); - - expect(schema.safeParse({ q: [1] }).success).toBeTruthy(); - expect(schema.safeParse({ q: ['abc'] }).success).toBeFalsy(); - - expect(schema.safeParse({ r: 'abc' }).success).toBeFalsy(); - expect(schema.safeParse({ r: randomUUID() }).success).toBeTruthy(); - }); - - it('refinement scalar fields', async () => { - const model = ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - model M { - id Int @id @default(autoincrement()) - email String? - x Int? - dtStr String? - url String? - dt DateTime? - s1 String? - s2 String? - s3 String? - - @@validate(email(email) && x > 0, 'condition1') - @@validate(length(email, 5, 10), 'condition2') - @@validate(regex(email, 'a[b|c]d'), 'condition3') - @@validate(dtStr == null || datetime(dtStr), 'condition4') - @@validate(!url || url(url), 'condition5') - @@validate(!dt || dt < now(), 'condition6') - @@validate(!s1 || contains(s1, 'abc'), 'condition7') - @@validate(!s2 || startsWith(s2, 'abc'), 'condition8') - @@validate(!s3 || endsWith(s3, 'abc'), 'condition9') - } - `; - - const { zodSchemas } = await loadSchema(model, { addPrelude: false, pushDb: false }); - - const schema = zodSchemas.models.MCreateSchema; - expect(schema.safeParse({ email: 'abd@x.com', x: 0 }).error.toString()).toMatch(/condition1/); - expect(schema.safeParse({ email: 'abd@x.com', x: 1 }).success).toBeTruthy(); - expect(schema.safeParse({ email: 'someone@microsoft.com', x: 1 }).error.toString()).toMatch(/condition2/); - expect(schema.safeParse({ email: 'xyz@x.com', x: 1 }).error.toString()).toMatch(/condition3/); - - expect(schema.safeParse({ dtStr: '2020-01' }).error.toString()).toMatch(/condition4/); - expect(schema.safeParse({ email: 'abd@x.com', x: 1, dtStr: '2020-01-01T00:00:00+02:00' }).success).toBeTruthy(); - - expect(schema.safeParse({ url: 'xyz.com' }).error.toString()).toMatch(/condition5/); - expect(schema.safeParse({ email: 'abd@x.com', x: 1, url: 'https://zenstack.dev' }).success).toBeTruthy(); - - expect(schema.safeParse({ email: 'abd@x.com', x: 1, dt: new Date('2030-01-01') }).error.toString()).toMatch( - /condition6/ - ); - expect(schema.safeParse({ email: 'abd@x.com', x: 1, dt: new Date('2020-01-01') }).success).toBeTruthy(); - - expect(schema.safeParse({ email: 'abd@x.com', x: 1, s1: '1abc2' }).success).toBeTruthy(); - expect(schema.safeParse({ s1: 'abd' }).error.toString()).toMatch(/condition7/); - - expect(schema.safeParse({ email: 'abd@x.com', x: 1, s2: 'abc1' }).success).toBeTruthy(); - expect(schema.safeParse({ s2: '1abc' }).error.toString()).toMatch(/condition8/); - - expect(schema.safeParse({ email: 'abd@x.com', x: 1, s3: '1abc' }).success).toBeTruthy(); - expect(schema.safeParse({ s3: 'abc1' }).error.toString()).toMatch(/condition9/); - }); - - it('refinement collection fields', async () => { - const model = ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - } - - model M { - id Int @id @default(autoincrement()) - arr Int[] - - @@validate(!isEmpty(arr), 'condition1') - @@validate(has(arr, 1), 'condition2') - @@validate(hasEvery(arr, [1, 2]), 'condition3') - @@validate(hasSome(arr, [1, 2]), 'condition4') - } - `; - - const { zodSchemas } = await loadSchema(model, { addPrelude: false, pushDb: false }); - - const schema = zodSchemas.models.MCreateSchema; - expect(schema.safeParse({}).error.toString()).toMatch(/condition1/); - expect(schema.safeParse({ arr: [] }).error.toString()).toMatch(/condition1/); - expect(schema.safeParse({ arr: [3] }).error.toString()).toMatch(/condition2/); - expect(schema.safeParse({ arr: [1] }).error.toString()).toMatch(/condition3/); - expect(schema.safeParse({ arr: [4] }).error.toString()).toMatch(/condition4/); - expect(schema.safeParse({ arr: [1, 2, 3] }).success).toBeTruthy(); - }); - - it('refinement with path', async () => { - const model = ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - } - - model M { - id Int @id @default(autoincrement()) - arr Int[] - - @@validate(!isEmpty(arr), 'condition1', ['array']) - @@validate(has(arr, 1), 'condition2', ['arr']) - @@validate(hasEvery(arr, [1, 2]), 'condition3', ['arr', 'every']) - @@validate(hasSome(arr, [1, 2]), 'condition4', ['arr', 'some']) - } - `; - - const { zodSchemas } = await loadSchema(model, { addPrelude: false, pushDb: false }); - - const schema = zodSchemas.models.MCreateSchema; - expect(schema.safeParse({}).error.issues[0].path).toEqual(['array']); - expect(schema.safeParse({ arr: [] }).error.issues[0].path).toEqual(['array']); - expect(schema.safeParse({ arr: [3] }).error.issues[0].path).toEqual(['arr']); - expect(schema.safeParse({ arr: [3] }).error.issues[1].path).toEqual(['arr', 'every']); - expect(schema.safeParse({ arr: [3] }).error.issues[2].path).toEqual(['arr', 'some']); - expect(schema.safeParse({ arr: [1] }).error.issues[0].path).toEqual(['arr', 'every']); - expect(schema.safeParse({ arr: [4] }).error.issues[0].path).toEqual(['arr']); - expect(schema.safeParse({ arr: [4] }).error.issues[1].path).toEqual(['arr', 'every']); - expect(schema.safeParse({ arr: [4] }).error.issues[2].path).toEqual(['arr', 'some']); - expect(schema.safeParse({ arr: [1, 2, 3] }).success).toBeTruthy(); - }); - - it('full-text search', async () => { - const model = ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - previewFeatures = ["fullTextSearch"] - } - - plugin zod { - provider = "@core/zod" - } - - enum Role { - USER - ADMIN - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email @endsWith('@zenstack.dev') - password String @omit - role Role @default(USER) - posts post_Item[] - } - - model post_Item { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String @length(5, 10) - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - published Boolean @default(false) - viewCount Int @default(0) - } - `; - - await loadSchema(model, { addPrelude: false, pushDb: false }); - }); - - it('no unchecked input', async () => { - const { zodSchemas } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - noUncheckedInput = true - } - - enum Role { - USER - ADMIN - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email @endsWith('@zenstack.dev') - password String @omit - role Role @default(USER) - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String @length(5, 10) - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - published Boolean @default(false) - viewCount Int @default(0) - } - `, - { addPrelude: false, pushDb: false } - ); - const schemas = zodSchemas.models; - - // create unchecked - expect( - zodSchemas.input.UserInputSchema.create.safeParse({ - data: { id: 1, email: 'abc@zenstack.dev', password: 'abc123' }, - }).success - ).toBeFalsy(); - - // update unchecked - expect( - zodSchemas.input.UserInputSchema.update.safeParse({ where: { id: 1 }, data: { id: 2 } }).success - ).toBeFalsy(); - }); - - it('does date coercion', async () => { - const { zodSchemas } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - } - - model Model { - id Int @id @default(autoincrement()) - dt DateTime - } - `, - { addPrelude: false, pushDb: false } - ); - const schemas = zodSchemas.models; - - expect( - schemas.ModelCreateSchema.safeParse({ - dt: new Date(), - }).success - ).toBeTruthy(); - - expect( - schemas.ModelCreateSchema.safeParse({ - dt: '2023-01-01T00:00:00.000Z', - }).success - ).toBeTruthy(); - - expect( - schemas.ModelCreateSchema.safeParse({ - dt: '2023-13-01', - }).success - ).toBeFalsy(); - }); - - it('generate for selected models full', async () => { - const { projectDir } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - output = '$projectRoot/zod' - generateModels = ['post'] - } - - model User { - id String @id - email String @unique - posts post[] - foos foo[] - } - - model post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - } - - model foo { - id String @id - name String - owner User? @relation(fields: [ownerId], references: [id]) - ownerId String? - } - - model bar { - id String @id - name String - } - `, - { - addPrelude: false, - pushDb: false, - compile: true, - } - ); - - expect(fs.existsSync(path.join(projectDir, 'zod/objects/UserWhereInput.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/objects/PostWhereInput.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/objects/FooWhereInput.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/objects/BarWhereInput.schema.js'))).toBeFalsy(); - expect(fs.existsSync(path.join(projectDir, 'zod/input/UserInput.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/input/PostInput.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/input/FooInput.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/input/BarInput.schema.js'))).toBeFalsy(); - expect(fs.existsSync(path.join(projectDir, 'zod/models/User.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/models/Post.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/models/Foo.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/models/Bar.schema.js'))).toBeFalsy(); - }); - - it('generate for selected models model only', async () => { - const { projectDir } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = "@core/zod" - output = '$projectRoot/zod' - modelOnly = true - generateModels = ['post'] - } - - model User { - id String @id - email String @unique - posts post[] - foos foo[] - } - - model post { - id String @id - title String - author User? @relation(fields: [authorId], references: [id]) - authorId String? - } - - model foo { - id String @id - name String - owner User? @relation(fields: [ownerId], references: [id]) - ownerId String? - } - - model bar { - id String @id - name String - } - `, - { - addPrelude: false, - pushDb: false, - compile: true, - } - ); - - expect(fs.existsSync(path.join(projectDir, 'zod/models/Post.schema.js'))).toBeTruthy(); - expect(fs.existsSync(path.join(projectDir, 'zod/models/User.schema.js'))).toBeFalsy(); - expect(fs.existsSync(path.join(projectDir, 'zod/models/Foo.schema.js'))).toBeFalsy(); - expect(fs.existsSync(path.join(projectDir, 'zod/models/Bar.schema.js'))).toBeFalsy(); - }); - - it('clear output', async () => { - const { name: projectDir } = tmp.dirSync(); - fs.mkdirSync(path.join(projectDir, 'zod'), { recursive: true }); - fs.writeFileSync(path.join(projectDir, 'zod', 'test.txt'), 'hello'); - - await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin enhancer { - provider = '@core/enhancer' - output = "$projectRoot/enhance" - } - - plugin zod { - provider = "@core/zod" - output = "$projectRoot/zod" - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email @endsWith('@zenstack.dev') - password String @omit - } - `, - { addPrelude: false, pushDb: false, projectDir, getPrismaOnly: true, generateNoCompile: true } - ); - - expect(fs.existsSync(path.join(projectDir, 'zod', 'test.txt'))).toBeFalsy(); - }); - - it('existing output as file', async () => { - const { name: projectDir } = tmp.dirSync(); - fs.writeFileSync(path.join(projectDir, 'zod'), 'hello'); - - await expect( - loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin enhancer { - provider = '@core/enhancer' - output = "$projectRoot/enhance" - } - - plugin zod { - provider = "@core/zod" - output = "$projectRoot/zod" - } - - model User { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email @endsWith('@zenstack.dev') - password String @omit - } - `, - { addPrelude: false, pushDb: false, projectDir, generateNoCompile: true } - ) - ).rejects.toThrow('already exists and is not a directory'); - }); - - it('is strict by default', async () => { - const { zodSchemas } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = '@core/zod' - } - - model User { - id Int @id @default(autoincrement()) - email String @unique @email @endsWith('@zenstack.dev') - password String - @@validate(length(password, 6, 20)) - } - `, - { addPrelude: false, pushDb: false } - ); - - const schemas = zodSchemas.models; - expect( - schemas.UserSchema.safeParse({ id: 1, email: 'abc@zenstack.dev', password: 'abc123' }).success - ).toBeTruthy(); - expect( - schemas.UserSchema.safeParse({ id: 1, email: 'abc@zenstack.dev', password: 'abc123', x: 1 }).success - ).toBeFalsy(); - - expect( - schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', password: 'abc123' }).success - ).toBeTruthy(); - expect( - schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', password: 'abc123', x: 1 }).success - ).toBeFalsy(); - - // Prisma create/update schema should always non-strict - expect( - schemas.UserPrismaCreateSchema.safeParse({ email: 'abc@zenstack.dev', password: 'abc123', x: 1 }).data.x - ).toBe(1); - expect(schemas.UserPrismaUpdateSchema.safeParse({ x: 1 }).data.x).toBe(1); - - expect( - zodSchemas.input.UserInputSchema.create.safeParse({ - data: { id: 1, email: 'abc@zenstack.dev', password: 'abc123' }, - }).success - ).toBeTruthy(); - expect( - zodSchemas.input.UserInputSchema.create.safeParse({ - data: { id: 1, email: 'abc@zenstack.dev', password: 'abc123', x: 1 }, - }).success - ).toBeFalsy(); - }); - - it('works in strip mode', async () => { - const { zodSchemas } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = '@core/zod' - mode = 'strip' - } - - model User { - id Int @id @default(autoincrement()) - email String @unique @email @endsWith('@zenstack.dev') - password String - @@validate(length(password, 6, 20)) - } - `, - { addPrelude: false, pushDb: false } - ); - - const schemas = zodSchemas.models; - let parsed = schemas.UserSchema.safeParse({ id: 1, email: 'abc@zenstack.dev', password: 'abc123', x: 1 }); - expect(parsed.success).toBeTruthy(); - expect(parsed.data.x).toBeUndefined(); - - parsed = schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', password: 'abc123', x: 1 }); - expect(parsed.success).toBeTruthy(); - expect(parsed.data.x).toBeUndefined(); - - parsed = zodSchemas.input.UserInputSchema.create.safeParse({ - data: { id: 1, email: 'abc@zenstack.dev', password: 'abc123', x: 1 }, - }); - expect(parsed.success).toBeTruthy(); - expect(parsed.data.data.x).toBeUndefined(); - }); - - it('works in passthrough mode', async () => { - const { zodSchemas } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = '@core/zod' - mode = 'passthrough' - } - - model User { - id Int @id @default(autoincrement()) - email String @unique @email @endsWith('@zenstack.dev') - password String - @@validate(length(password, 6, 20)) - } - `, - { addPrelude: false, pushDb: false } - ); - - const schemas = zodSchemas.models; - let parsed = schemas.UserSchema.safeParse({ id: 1, email: 'abc@zenstack.dev', password: 'abc123', x: 1 }); - expect(parsed.success).toBeTruthy(); - expect(parsed.data.x).toBe(1); - - parsed = schemas.UserCreateSchema.safeParse({ email: 'abc@zenstack.dev', password: 'abc123', x: 1 }); - expect(parsed.success).toBeTruthy(); - expect(parsed.data.x).toBe(1); - - parsed = zodSchemas.input.UserInputSchema.create.safeParse({ - data: { id: 1, email: 'abc@zenstack.dev', password: 'abc123', x: 1 }, - }); - expect(parsed.success).toBeTruthy(); - expect(parsed.data.data.x).toBe(1); - }); - - it('complains about invalid mode', async () => { - await expect( - loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = '@core/zod' - mode = 'xyz' - } - - model User { - id Int @id @default(autoincrement()) - email String @unique @email @endsWith('@zenstack.dev') - password String - @@validate(length(password, 6, 20)) - } - `, - { addPrelude: false, pushDb: false } - ) - ).rejects.toThrow(/Invalid mode/); - }); - - it('supports type def', async () => { - const { zodSchemas } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - } - - generator js { - provider = 'prisma-client-js' - } - - plugin zod { - provider = '@core/zod' - } - - type Address { - city String @length(2, 20) - } - - type Profile { - age Int @gte(18) - address Address? - } - - model User { - id Int @id @default(autoincrement()) - profile Profile @json - } - `, - { addPrelude: false, pushDb: false } - ); - - const schemas = zodSchemas.models; - - let parsed = schemas.ProfileSchema.safeParse({ age: 18, address: { city: 'NY' } }); - expect(parsed.success).toBeTruthy(); - expect(parsed.data).toEqual({ age: 18, address: { city: 'NY' } }); - - expect(schemas.ProfileSchema.safeParse({ age: 18 })).toMatchObject({ success: true }); - expect(schemas.ProfileSchema.safeParse({ age: 10 })).toMatchObject({ success: false }); - expect(schemas.ProfileSchema.safeParse({ address: { city: 'NY' } })).toMatchObject({ success: false }); - expect(schemas.ProfileSchema.safeParse({ address: { age: 18, city: 'N' } })).toMatchObject({ success: false }); - - expect(schemas.UserSchema.safeParse({ id: 1, profile: { age: 18 } })).toMatchObject({ success: true }); - expect(schemas.UserSchema.safeParse({ id: 1, profile: { age: 10 } })).toMatchObject({ success: false }); - - const objectSchemas = zodSchemas.objects; - expect(objectSchemas.UserCreateInputObjectSchema.safeParse({ profile: { age: 18 } })).toMatchObject({ - success: true, - }); - expect(objectSchemas.UserCreateInputObjectSchema.safeParse({ profile: { age: 10 } })).toMatchObject({ - success: false, - }); - }); - - it('ignores "@ignore" fields', async () => { - const { zodSchemas } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - email String @unique - password String @ignore - } - ` - ); - - const schemas = zodSchemas.models; - expect(schemas.UserSchema.safeParse({ id: 1, email: 'a@b.com' }).success).toBeTruthy(); - expect(schemas.UserPrismaCreateSchema.safeParse({ email: 'a@b.com' }).success).toBeTruthy(); - }); - - it('@json fields with @default should be optional', async () => { - const { zodSchemas } = await loadSchema( - ` - type Foo { - a String - } - - model Bar { - id Int @id @default(autoincrement()) - foo Foo @json @default("{ \\"a\\": \\"a\\" }") - fooList Foo[] @json @default("[]") - } - `, - { - fullZod: true, - provider: 'postgresql', - pushDb: false, - } - ); - - // Ensure Zod Schemas correctly mark @default fields as optional - expect(zodSchemas.objects.BarCreateInputObjectSchema.safeParse({}).success).toBeTruthy(); - }); -}); diff --git a/tests/integration/tests/schema/cal-com.zmodel b/tests/integration/tests/schema/cal-com.zmodel deleted file mode 100644 index bc693d8d0..000000000 --- a/tests/integration/tests/schema/cal-com.zmodel +++ /dev/null @@ -1,654 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -generator client { - provider = "prisma-client-js" - previewFeatures = [] -} - -enum SchedulingType { - ROUND_ROBIN @map("roundRobin") - COLLECTIVE @map("collective") -} - -enum PeriodType { - UNLIMITED @map("unlimited") - ROLLING @map("rolling") - RANGE @map("range") -} - -model Host { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - eventTypeId Int - isFixed Boolean @default(false) -} - -model EventType { - id Int @id @default(autoincrement()) - /// @zod.min(1) - title String - /// @zod.custom(imports.eventTypeSlug) - slug String - description String? - position Int @default(0) - /// @zod.custom(imports.eventTypeLocations) - locations Json? - length Int - hidden Boolean @default(false) - hosts Host[] - users User[] @relation("user_eventtype") - owner User? @relation("owner", fields: [userId], references: [id], onDelete: Cascade) - userId Int? - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - teamId Int? - hashedLink HashedLink? - bookings Booking[] - availability Availability[] - webhooks Webhook[] - destinationCalendar DestinationCalendar? - eventName String? - customInputs EventTypeCustomInput[] - /// @zod.custom(imports.eventTypeBookingFields) - bookingFields Json? - timeZone String? - periodType PeriodType @default(UNLIMITED) - periodStartDate DateTime? - periodEndDate DateTime? - periodDays Int? - periodCountCalendarDays Boolean? - requiresConfirmation Boolean @default(false) - /// @zod.custom(imports.recurringEventType) - recurringEvent Json? - disableGuests Boolean @default(false) - hideCalendarNotes Boolean @default(false) - minimumBookingNotice Int @default(120) - beforeEventBuffer Int @default(0) - afterEventBuffer Int @default(0) - seatsPerTimeSlot Int? - seatsShowAttendees Boolean? @default(false) - schedulingType SchedulingType? - schedule Schedule? @relation(fields: [scheduleId], references: [id]) - scheduleId Int? - // price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column. - price Int @default(0) - // currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column. - currency String @default("usd") - slotInterval Int? - /// @zod.custom(imports.EventTypeMetaDataSchema) - metadata Json? - /// @zod.custom(imports.successRedirectUrl) - successRedirectUrl String? - workflows WorkflowsOnEventTypes[] - /// @zod.custom(imports.bookingLimitsType) - bookingLimits Json? - @@unique([userId, slug]) - @@unique([teamId, slug]) -} - -model Credential { - id Int @id @default(autoincrement()) - // @@type is deprecated - type String - key Json - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int? - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - // How to make it a required column? - appId String? - destinationCalendars DestinationCalendar[] - invalid Boolean? @default(false) -} - -enum IdentityProvider { - CAL - GOOGLE - SAML -} - -model DestinationCalendar { - id Int @id @default(autoincrement()) - integration String - externalId String - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int? @unique - booking Booking[] - eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - eventTypeId Int? @unique - credentialId Int? - credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade) -} - -enum UserPermissionRole { - USER - ADMIN -} - -model User { - id Int @id @default(autoincrement()) - username String? @unique - name String? - /// @zod.email() - email String @unique - emailVerified DateTime? - password String? - bio String? - avatar String? - timeZone String @default("Europe/London") - weekStart String @default("Sunday") - // DEPRECATED - TO BE REMOVED - startTime Int @default(0) - endTime Int @default(1440) - // - bufferTime Int @default(0) - hideBranding Boolean @default(false) - theme String? - createdDate DateTime @default(now()) @map(name: "created") - trialEndsAt DateTime? - eventTypes EventType[] @relation("user_eventtype") - credentials Credential[] - teams Membership[] - bookings Booking[] - schedules Schedule[] - defaultScheduleId Int? - selectedCalendars SelectedCalendar[] - completedOnboarding Boolean @default(false) - locale String? - timeFormat Int? @default(12) - twoFactorSecret String? - twoFactorEnabled Boolean @default(false) - identityProvider IdentityProvider @default(CAL) - identityProviderId String? - availability Availability[] - invitedTo Int? - webhooks Webhook[] - brandColor String @default("#292929") - darkBrandColor String @default("#fafafa") - // the location where the events will end up - destinationCalendar DestinationCalendar? - away Boolean @default(false) - // participate in dynamic group booking or not - allowDynamicBooking Boolean? @default(true) - /// @zod.custom(imports.userMetadata) - metadata Json? - verified Boolean? @default(false) - role UserPermissionRole @default(USER) - disableImpersonation Boolean @default(false) - impersonatedUsers Impersonations[] @relation("impersonated_user") - impersonatedBy Impersonations[] @relation("impersonated_by_user") - apiKeys ApiKey[] - accounts Account[] - sessions Session[] - Feedback Feedback[] - ownedEventTypes EventType[] @relation("owner") - workflows Workflow[] - routingForms App_RoutingForms_Form[] @relation("routing-form") - verifiedNumbers VerifiedNumber[] - hosts Host[] - @@map(name: "users") -} - -model Team { - id Int @id @default(autoincrement()) - /// @zod.min(1) - name String - /// @zod.min(1) - slug String? @unique - logo String? - bio String? - hideBranding Boolean @default(false) - hideBookATeamMember Boolean @default(false) - members Membership[] - eventTypes EventType[] - workflows Workflow[] - createdAt DateTime @default(now()) - /// @zod.custom(imports.teamMetadataSchema) - metadata Json? - theme String? - brandColor String @default("#292929") - darkBrandColor String @default("#fafafa") - verifiedNumbers VerifiedNumber[] -} - -enum MembershipRole { - MEMBER - ADMIN - OWNER -} - -model Membership { - teamId Int - userId Int - accepted Boolean @default(false) - role MembershipRole - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - disableImpersonation Boolean @default(false) - @@id([userId, teamId]) -} - -model VerificationToken { - id Int @id @default(autoincrement()) - identifier String - token String @unique - expires DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - @@unique([identifier, token]) -} - -model BookingReference { - id Int @id @default(autoincrement()) - /// @zod.min(1) - type String - /// @zod.min(1) - uid String - meetingId String? - meetingPassword String? - meetingUrl String? - booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade) - bookingId Int? - externalCalendarId String? - deleted Boolean? - credentialId Int? -} - -model Attendee { - id Int @id @default(autoincrement()) - email String - name String - timeZone String - locale String? @default("en") - booking Booking? @relation(fields: [bookingId], references: [id]) - bookingId Int? -} - -enum BookingStatus { - CANCELLED @map("cancelled") - ACCEPTED @map("accepted") - REJECTED @map("rejected") - PENDING @map("pending") -} - -model Booking { - id Int @id @default(autoincrement()) - uid String @unique - user User? @relation(fields: [userId], references: [id]) - userId Int? - references BookingReference[] - eventType EventType? @relation(fields: [eventTypeId], references: [id]) - eventTypeId Int? - title String - description String? - customInputs Json? - /// @zod.custom(imports.bookingResponses) - responses Json? - startTime DateTime - endTime DateTime - attendees Attendee[] - location String? - createdAt DateTime @default(now()) - updatedAt DateTime? - status BookingStatus @default(ACCEPTED) - paid Boolean @default(false) - payment Payment[] - destinationCalendar DestinationCalendar? @relation(fields: [destinationCalendarId], references: [id]) - destinationCalendarId Int? - cancellationReason String? - rejectionReason String? - dynamicEventSlugRef String? - dynamicGroupSlugRef String? - rescheduled Boolean? - fromReschedule String? - recurringEventId String? - smsReminderNumber String? - workflowReminders WorkflowReminder[] - scheduledJobs String[] - /// @zod.custom(imports.bookingMetadataSchema) - metadata Json? -} - -model Schedule { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - eventType EventType[] - name String - timeZone String? - availability Availability[] - @@index([userId]) -} - -model Availability { - id Int @id @default(autoincrement()) - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int? - eventType EventType? @relation(fields: [eventTypeId], references: [id]) - eventTypeId Int? - days Int[] - startTime DateTime @db.Time - endTime DateTime @db.Time - date DateTime? @db.Date - Schedule Schedule? @relation(fields: [scheduleId], references: [id]) - scheduleId Int? - @@index([eventTypeId]) - @@index([scheduleId]) -} - -model SelectedCalendar { - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - integration String - externalId String - @@id([userId, integration, externalId]) -} - -enum EventTypeCustomInputType { - TEXT @map("text") - TEXTLONG @map("textLong") - NUMBER @map("number") - BOOL @map("bool") - RADIO @map("radio") - PHONE @map("phone") -} - -model EventTypeCustomInput { - id Int @id @default(autoincrement()) - eventTypeId Int - eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - label String - type EventTypeCustomInputType - /// @zod.custom(imports.customInputOptionSchema) - options Json? - required Boolean - placeholder String @default("") -} - -model ResetPasswordRequest { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String - expires DateTime -} - -enum ReminderType { - PENDING_BOOKING_CONFIRMATION -} - -model ReminderMail { - id Int @id @default(autoincrement()) - referenceId Int - reminderType ReminderType - elapsedMinutes Int - createdAt DateTime @default(now()) -} - -model Payment { - id Int @id @default(autoincrement()) - uid String @unique - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - appId String? - bookingId Int - booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade) - amount Int - fee Int - currency String - success Boolean - refunded Boolean - data Json - externalId String @unique -} - -enum WebhookTriggerEvents { - BOOKING_CREATED - BOOKING_RESCHEDULED - BOOKING_CANCELLED - FORM_SUBMITTED - MEETING_ENDED -} - -model Webhook { - id String @id @unique - userId Int? - eventTypeId Int? - /// @zod.url() - subscriberUrl String - payloadTemplate String? - createdAt DateTime @default(now()) - active Boolean @default(true) - eventTriggers WebhookTriggerEvents[] - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - appId String? - secret String? - @@unique([userId, subscriberUrl], name: "courseIdentifier") -} - -model Impersonations { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - impersonatedUser User @relation("impersonated_user", fields: [impersonatedUserId], references: [id], onDelete: Cascade) - impersonatedBy User @relation("impersonated_by_user", fields: [impersonatedById], references: [id], onDelete: Cascade) - impersonatedUserId Int - impersonatedById Int -} - -model ApiKey { - id String @id @unique @default(cuid()) - userId Int - note String? - createdAt DateTime @default(now()) - expiresAt DateTime? - lastUsedAt DateTime? - hashedKey String @unique() - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - app App? @relation(fields: [appId], references: [slug], onDelete: Cascade) - appId String? -} - -model HashedLink { - id Int @id @default(autoincrement()) - link String @unique() - eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - eventTypeId Int @unique -} - -model Account { - id String @id @default(cuid()) - userId Int - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([provider, providerAccountId]) -} - -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId Int - expires DateTime - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) -} - -enum AppCategories { - calendar - messaging - other - payment - video - web3 - automation - analytics -} - -model App { - // The slug for the app store public page inside `/apps/[slug]` - slug String @id @unique - // The directory name for `/packages/app-store/[dirName]` - dirName String @unique - // Needed API Keys - keys Json? - // One or multiple categories to which this app belongs - categories AppCategories[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - credentials Credential[] - payments Payment[] - Webhook Webhook[] - ApiKey ApiKey[] - enabled Boolean @default(false) -} - -model App_RoutingForms_Form { - id String @id @default(cuid()) - description String? - routes Json? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String - fields Json? - user User @relation("routing-form", fields: [userId], references: [id], onDelete: Cascade) - userId Int - responses App_RoutingForms_FormResponse[] - disabled Boolean @default(false) - /// @zod.custom(imports.RoutingFormSettings) - settings Json? -} - -model App_RoutingForms_FormResponse { - id Int @id @default(autoincrement()) - formFillerId String @default(cuid()) - form App_RoutingForms_Form @relation(fields: [formId], references: [id], onDelete: Cascade) - formId String - response Json - createdAt DateTime @default(now()) - @@unique([formFillerId, formId]) -} - -model Feedback { - id Int @id @default(autoincrement()) - date DateTime @default(now()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - rating String - comment String? -} - -enum WorkflowTriggerEvents { - BEFORE_EVENT - EVENT_CANCELLED - NEW_EVENT - AFTER_EVENT - RESCHEDULE_EVENT -} - -enum WorkflowActions { - EMAIL_HOST - EMAIL_ATTENDEE - SMS_ATTENDEE - SMS_NUMBER - EMAIL_ADDRESS -} - -model WorkflowStep { - id Int @id @default(autoincrement()) - stepNumber Int - action WorkflowActions - workflowId Int - workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade) - sendTo String? - reminderBody String? - emailSubject String? - template WorkflowTemplates @default(REMINDER) - workflowReminders WorkflowReminder[] - numberRequired Boolean? - sender String? - numberVerificationPending Boolean @default(true) -} - -model Workflow { - id Int @id @default(autoincrement()) - name String - userId Int? - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - teamId Int? - activeOn WorkflowsOnEventTypes[] - trigger WorkflowTriggerEvents - time Int? - timeUnit TimeUnit? - steps WorkflowStep[] -} - -model WorkflowsOnEventTypes { - id Int @id @default(autoincrement()) - workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade) - workflowId Int - eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) - eventTypeId Int -} - -model Deployment { - /// This is a single row table, so we use a fixed id - id Int @id @default(1) - logo String? - /// @zod.custom(imports.DeploymentTheme) - theme Json? - licenseKey String? - agreedLicenseAt DateTime? -} - -enum TimeUnit { - DAY @map("day") - HOUR @map("hour") - MINUTE @map("minute") -} - -model WorkflowReminder { - id Int @id @default(autoincrement()) - bookingUid String - booking Booking? @relation(fields: [bookingUid], references: [uid], onDelete: Cascade) - method WorkflowMethods - scheduledDate DateTime - referenceId String? @unique - scheduled Boolean - workflowStepId Int - workflowStep WorkflowStep @relation(fields: [workflowStepId], references: [id], onDelete: Cascade) - cancelled Boolean? -} - -enum WorkflowTemplates { - REMINDER - CUSTOM -} - -enum WorkflowMethods { - EMAIL - SMS -} - -model VerifiedNumber { - id Int @id @default(autoincrement()) - userId Int? - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - teamId Int? - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - phoneNumber String -} diff --git a/tests/integration/tests/schema/petstore.zmodel b/tests/integration/tests/schema/petstore.zmodel deleted file mode 100644 index 8f032b510..000000000 --- a/tests/integration/tests/schema/petstore.zmodel +++ /dev/null @@ -1,52 +0,0 @@ -datasource db { - provider = 'sqlite' - url = 'file:./petstore.db' -} - -generator js { - provider = 'prisma-client-js' -} - -plugin zod { - provider = '@core/zod' -} - -model User { - id String @id @default(cuid()) - email String @unique - orders Order[] - - // everybody can signup - @@allow('create', true) - - // user profile is publicly readable - @@allow('read', true) -} - -model Pet { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String - category String - order Order? @relation(fields: [orderId], references: [id]) - orderId String? - - // unsold pets are readable to all; sold ones are readable to buyers only - @@allow('read', orderId == null || order.user == auth()) - - // only allow update to 'orderId' field if it's not set yet (unsold) - @@allow('update', name == future().name && category == future().category && orderId == null ) -} - -model Order { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - pets Pet[] - user User @relation(fields: [userId], references: [id]) - userId String - - // users can read their orders - @@allow('read,create', auth() == user) -} diff --git a/tests/integration/tests/schema/refactor-pg.zmodel b/tests/integration/tests/schema/refactor-pg.zmodel deleted file mode 100644 index beb5c2125..000000000 --- a/tests/integration/tests/schema/refactor-pg.zmodel +++ /dev/null @@ -1,91 +0,0 @@ -enum Role { - USER - ADMIN -} - -model User { - id Int @id @default(autoincrement()) - email String @unique @email @lower - role Role @default(USER) - profile Profile? - posts Post[] - comments Comment[] - - // everybody can signup - @@allow('create', true) - - @@allow('read', auth() != null) - - // full-access by self - @@allow('all', auth() == this || auth().role == ADMIN) -} - -model Profile { - id Int @id @default(autoincrement()) - name String - homepage String? @url - private Boolean @default(false) - image Image? @relation(fields: [imageId], references: [id], onDelete: Cascade) - imageId Int? @unique - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int @unique - - // user profile is publicly readable - @@allow('read', auth() != null && !private) - - // user profile is only updatable by the user - @@allow('all', auth() == user || auth().role == ADMIN) -} - -model Image { - id Int @id @default(autoincrement()) - url String @url - profile Profile? - - comment Comment? @relation(fields: [commentId], references: [id]) - commentId Int? - - @@allow('create,read', true) - @@allow('update,delete', auth().role == ADMIN) -} - -model Post { - id Int @id @default(autoincrement()) - title String @length(1, 8) @trim - published Boolean @default(false) - comments Comment[] - author User @relation(fields: [authorId], references: [id], onDelete: Cascade) - authorId Int - - // posts are readable by all - @@allow('read', published) - - // posts are updatable by the author - @@allow('all', auth() == author || auth().role == ADMIN) -} - -model Comment { - id Int @id @default(autoincrement()) - content String @trim - - author User @relation(fields: [authorId], references: [id], onDelete: Cascade) - authorId Int - - post Post @relation(fields: [postId], references: [id], onDelete: Cascade) - postId Int - - images Image[] - - // comments are readable by all - @@allow('read', post.published) - - @@allow('create', auth() != null && post.published && auth() == author) - - @@allow('update', auth() == author && future().author == auth()) - - @@allow('delete', auth() == author || auth() == post.author) - - // comments are updatable by the author - @@allow('all', auth().role == ADMIN) -} diff --git a/tests/integration/tests/schema/todo-pg.zmodel b/tests/integration/tests/schema/todo-pg.zmodel deleted file mode 100644 index 05255da44..000000000 --- a/tests/integration/tests/schema/todo-pg.zmodel +++ /dev/null @@ -1,143 +0,0 @@ -/* -* Sample model for a collaborative Todo app -*/ - -enum UserRole { - ADMIN - USER -} - -/* - * Model for a space in which users can collaborate on Lists and Todos - */ -model Space { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String @length(4, 50) - slug String @unique @length(4, 16) - owner User? @relation(fields: [ownerId], references: [id]) - ownerId String? - members SpaceUser[] - lists List[] - - // require login - @@deny('all', auth() == null) - - // everyone can create a space - @@allow('create', true) - - // any user in the space can read the space - @@allow('read', members?[user == auth()]) - - // space admin can update and delete - @@allow('update,delete', members?[user == auth() && role == ADMIN]) -} - -/* - * Model representing membership of a user in a space - */ -model SpaceUser { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - role UserRole - @@unique([userId, spaceId]) - - // require login - @@deny('all', auth() == null) - - // space admin can create/update/delete - @@allow('create,update,delete', space.members?[user == auth() && role == ADMIN]) - - // user can read entries for spaces which he's a member of - @@allow('read', space.members?[user == auth()]) -} - -/* - * Model for a user - */ -model User { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email - password String? @password @omit - emailVerified DateTime? - name String? - ownedSpaces Space[] - spaces SpaceUser[] - image String? @url - lists List[] - todos Todo[] - - // can be created by anyone, even not logged in - @@allow('create', true) - - // can be read by users sharing any space - @@allow('read', spaces?[space.members?[user == auth()]]) - - // full access by oneself - @@allow('all', auth() == this) -} - -/* - * Model for a Todo list - */ -model List { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - title String @length(1, 100) - private Boolean @default(false) - todos Todo[] - - // require login - @@deny('all', auth() == null) - - // can be read by owner or space members (only if not private) - @@allow('read', owner == auth() || (space.members?[user == auth()] && !private)) - - // when create, owner must be set to current user, and user must be in the space - @@allow('create', owner == auth() && space.members?[user == auth()]) - - // when create, owner must be set to current user, and user must be in the space - // update is not allowed to change owner - @@allow('update', owner == auth()&& space.members?[user == auth()] && future().owner == owner) - - // can be deleted by owner - @@allow('delete', owner == auth()) -} - -/* - * Model for a single Todo - */ -model Todo { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - list List @relation(fields: [listId], references: [id], onDelete: Cascade) - listId String - title String @length(1, 100) - completedAt DateTime? - - // require login - @@deny('all', auth() == null) - - // owner has full access, also space members have full access (if the parent List is not private) - @@allow('all', list.owner == auth()) - @@allow('all', list.space.members?[user == auth()] && !list.private) - - // update is not allowed to change owner - @@deny('update', future().owner != owner) -} diff --git a/tests/integration/tests/schema/todo.zmodel b/tests/integration/tests/schema/todo.zmodel deleted file mode 100644 index 8b045ea3a..000000000 --- a/tests/integration/tests/schema/todo.zmodel +++ /dev/null @@ -1,154 +0,0 @@ -/* -* Sample model for a collaborative Todo app -*/ - -datasource db { - provider = 'sqlite' - url = 'file:./test.db' -} - -generator js { - provider = 'prisma-client-js' -} - -plugin zod { - provider = '@core/zod' - // preserveTsFiles = true -} - -/* - * Model for a space in which users can collaborate on Lists and Todos - */ -model Space { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String @length(4, 50) - slug String @unique @length(4, 16) - owner User? @relation(fields: [ownerId], references: [id]) - ownerId String? - members SpaceUser[] - lists List[] - - // require login - @@deny('all', auth() == null) - - // everyone can create a space - @@allow('create', true) - - // any user in the space can read the space - @@allow('read', members?[user == auth()]) - - // space admin can update and delete - @@allow('update,delete', members?[user == auth() && role == 'ADMIN']) -} - -/* - * Model representing membership of a user in a space - */ -model SpaceUser { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - role String - @@unique([userId, spaceId]) - - // require login - @@deny('all', auth() == null) - - // space admin can create/update/delete - @@allow('create,update,delete', space.members?[user == auth() && role == 'ADMIN']) - - // user can read entries for spaces which he's a member of - @@allow('read', space.members?[user == auth()]) -} - -/* - * Model for a user - */ -model User { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email - password String? @password @omit - emailVerified DateTime? - name String? - bio String @ignore - ownedSpaces Space[] - spaces SpaceUser[] - image String? @url - lists List[] - todos Todo[] - - // can be created by anyone, even not logged in - @@allow('create', true) - - // can be read by users sharing any space - @@allow('read', spaces?[space.members?[user == auth()]]) - - // full access by oneself - @@allow('all', auth() == this) -} - -/* - * Model for a Todo list - */ -model List { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - title String @length(1, 100) - private Boolean @default(false) - todos Todo[] - revision Int @default(0) - - // require login - @@deny('all', auth() == null) - - // can be read by owner or space members (only if not private) - @@allow('read', owner == auth() || (space.members?[user == auth()] && !private)) - - // when create, owner must be set to current user, and user must be in the space - @@allow('create', owner == auth() && space.members?[user == auth()]) - - // when create, owner must be set to current user, and user must be in the space - // update is not allowed to change owner - @@allow('update', owner == auth() && space.members?[user == auth()] && future().owner == owner) - - // can be deleted by owner - @@allow('delete', owner == auth()) -} - -/* - * Model for a single Todo - */ -model Todo { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - list List @relation(fields: [listId], references: [id], onDelete: Cascade) - listId String - title String @length(1, 100) - completedAt DateTime? - - // require login - @@deny('all', auth() == null) - - // owner has full access, also space members have full access (if the parent List is not private) - @@allow('all', list.owner == auth()) - @@allow('all', list.space.members?[user == auth()] && !list.private) - - // update is not allowed to change owner - @@deny('update', future().owner != owner) -} diff --git a/tests/integration/tsconfig.json b/tests/integration/tsconfig.json deleted file mode 100644 index e11b3381d..000000000 --- a/tests/integration/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "experimentalDecorators": true, - "resolveJsonModule": true - }, - "include": ["**/*.ts", "**/*.d.ts"] -} diff --git a/tests/regression/.eslintrc.json b/tests/regression/.eslintrc.json deleted file mode 100644 index 24ebad85a..000000000 --- a/tests/regression/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "extends": ["plugin:jest/recommended"], - "rules": { - "jest/expect-expect": "off" - } -} diff --git a/tests/regression/jest.config.ts b/tests/regression/jest.config.ts deleted file mode 100644 index 67a118269..000000000 --- a/tests/regression/jest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import baseConfig from '../../jest.config'; - -/* - * For a detailed explanation regarding each configuration property and type check, visit: - * https://jestjs.io/docs/configuration - */ -export default { - ...baseConfig, - setupFilesAfterEnv: ['./test-setup.ts'], -}; diff --git a/tests/regression/jest.d.ts b/tests/regression/jest.d.ts deleted file mode 100644 index ff066029a..000000000 --- a/tests/regression/jest.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -interface CustomMatchers { - toBeRejectedByPolicy(expectedMessages?: string[]): Promise; - toBeNotFound(): Promise; - toResolveTruthy(): Promise; - toResolveFalsy(): Promise; - toResolveNull(): Promise; - toBeRejectedWithCode(code: string): Promise; -} -declare global { - namespace jest { - interface Expect extends CustomMatchers {} - interface Matchers extends CustomMatchers {} - interface InverseAsymmetricMatchers extends CustomMatchers {} - } -} -export {}; diff --git a/tests/regression/test-setup.ts b/tests/regression/test-setup.ts deleted file mode 100644 index 89142216d..000000000 --- a/tests/regression/test-setup.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - toBeNotFound, - toBeRejectedByPolicy, - toBeRejectedWithCode, - toResolveFalsy, - toResolveNull, - toResolveTruthy, -} from '@zenstackhq/testtools/jest-ext'; - -expect.extend({ - toBeRejectedByPolicy, - toBeNotFound, - toResolveTruthy, - toResolveFalsy, - toResolveNull, - toBeRejectedWithCode, -}); diff --git a/tests/regression/tests/issue-1014.test.ts b/tests/regression/tests/issue-1014.test.ts deleted file mode 100644 index 66caa1b11..000000000 --- a/tests/regression/tests/issue-1014.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1014', () => { - it('update', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id() @default(autoincrement()) - name String - posts Post[] - } - - model Post { - id Int @id() @default(autoincrement()) - title String - content String? - author User? @relation(fields: [authorId], references: [id]) - authorId Int? @allow('update', true, true) - - @@allow('read', true) - } - ` - ); - - const db = enhance(); - - const user = await prisma.user.create({ data: { name: 'User1' } }); - const post = await prisma.post.create({ data: { title: 'Post1' } }); - await expect(db.post.update({ where: { id: post.id }, data: { authorId: user.id } })).toResolveTruthy(); - }); - - it('read', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id() @default(autoincrement()) - title String @allow('read', true, true) - content String - } - ` - ); - - const db = enhance(); - - const post = await prisma.post.create({ data: { title: 'Post1', content: 'Content' } }); - await expect(db.post.findUnique({ where: { id: post.id } })).toResolveNull(); - await expect(db.post.findUnique({ where: { id: post.id }, select: { title: true } })).resolves.toEqual({ - title: 'Post1', - }); - }); -}); diff --git a/tests/regression/tests/issue-1058.test.ts b/tests/regression/tests/issue-1058.test.ts deleted file mode 100644 index cd566c71f..000000000 --- a/tests/regression/tests/issue-1058.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression for issue 1058', () => { - it('test', async () => { - const schema = ` - model User { - id String @id @default(cuid()) - name String - - userRankings UserRanking[] - userFavorites UserFavorite[] - } - - model Entity { - id String @id @default(cuid()) - name String - type String - userRankings UserRanking[] - userFavorites UserFavorite[] - - @@delegate(type) - } - - model Person extends Entity { - } - - model Studio extends Entity { - } - - - model UserRanking { - id String @id @default(cuid()) - rank Int - - entityId String - entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) - } - - model UserFavorite { - id String @id @default(cuid()) - - entityId String - entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) - } - `; - - await loadSchema(schema, { pushDb: false, provider: 'postgresql' }); - }); -}); diff --git a/tests/regression/tests/issue-1064.test.ts b/tests/regression/tests/issue-1064.test.ts deleted file mode 100644 index a8505f507..000000000 --- a/tests/regression/tests/issue-1064.test.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression for issue 1064', () => { - it('test', async () => { - const schema = ` - model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? // @db.Text - access_token String? // @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? // @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@allow('all', auth().id == userId) - @@unique([provider, providerAccountId]) - } - - model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@allow('all', auth().id == userId) - } - - model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@allow('all', true) - @@unique([identifier, token]) - } - - model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String - accounts Account[] - sessions Session[] - - username String @unique @length(min: 4, max: 20) - about String? @length(max: 500) - location String? @length(max: 100) - - role String @default("USER") @deny(operation: "update", auth().role != "ADMIN") - - inserted_at DateTime @default(now()) - updated_at DateTime @updatedAt() @default(now()) - - editComments EditComment[] - - posts Post[] - rankings UserRanking[] - ratings UserRating[] - favorites UserFavorite[] - - people Person[] - studios Studio[] - edits Edit[] - attachments Attachment[] - galleries Gallery[] - - uploads UserUpload[] - - maxUploadsPerDay Int @default(10) - maxEditsPerDay Int @default(10) - - // everyone can signup, and user profile is also publicly readable - @@allow('create,read', true) - // only the user can update or delete their own profile - @@allow('update,delete', auth() == this) - } - - abstract model UserEntityRelation { - entityId String? - entity Entity? @relation(fields: [entityId], references: [id], onUpdate: NoAction) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) - - - // everyone can read - @@allow('read', true) - @@allow('create,update,delete', auth().id == this.userId) - - @@unique([userId,entityId]) - } - - model UserUpload { - timestamp DateTime @default(now()) - - key String @id - url String @unique - size Int - - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) - - @@allow('create', auth().id == userId) - @@allow('all', auth().role == "ADMIN") - } - - model Post { - id Int @id @default(autoincrement()) - title String @length(max: 100) - body String @length(max: 1000) - createdAt DateTime @default(now()) - - authorId String - author User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: NoAction) - - @@allow('read', true) - @@allow('create,update,delete', auth().id == authorId && auth().role == "ADMIN") - } - - model Edit extends UserEntityRelation { - id String @id @default(cuid()) - status String @default("PENDING") @allow('update', auth().role in ["ADMIN", "MODERATOR"]) - type String @allow('update', false) - timestamp DateTime @default(now()) - note String? @length(max: 300) - // for creates - createPayload & updates - data before diff is applied - data String? - // for updates - diff String? - - comments EditComment[] - } - - model EditComment { - id Int @id @default(autoincrement()) - timestamp DateTime @default(now()) - content String @length(max: 300) - editId String - edit Edit @relation(fields: [editId], references: [id], onUpdate: Cascade) - authorId String - author User @relation(fields: [authorId], references: [id], onUpdate: Cascade) - - // everyone can read - @@allow('read', true) - @@allow('create,update,delete', auth().id == this.authorId || auth().role in ["ADMIN", "MODERATOR"]) - } - - model MetadataIdentifier { - id Int @default(autoincrement()) @id - - identifier String - - metadataSource String - MetadataSource MetadataSource @relation(fields: [metadataSource], references: [slug], onUpdate: Cascade) - - entities Entity[] - - @@unique([identifier, metadataSource]) - - @@allow('read', true) - @@allow('create,update,delete', auth().role in ["ADMIN", "MODERATOR"]) - } - - model MetadataSource { - slug String @id - name String @unique - identifierRegex String - desc String? - url String - icon String - identifiers MetadataIdentifier[] - - @@allow('all', auth().role == "ADMIN") - } - - model Attachment extends UserEntityRelation { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - key String @unique - url String @unique - galleries Gallery[] - @@allow('delete', auth().role in ["ADMIN", "MODERATOR"]) - } - - model Entity { - id String @id @default(cuid()) - name String - desc String? - - attachments Attachment[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt @default(now()) - - type String - - status String @default("PENDING") // PENDING ON INITIAL CREATION - verified Boolean @default(false) - - edits Edit[] - userRankings UserRanking[] - userFavorites UserFavorite[] - userRatings UserRating[] - metaIdentifiers MetadataIdentifier[] - - @@delegate(type) - - @@allow('read', true) - @@allow('create', auth() != null) - @@allow('update', auth().role in ["ADMIN", "MODERATOR"]) - @@allow('delete', auth().role == "ADMIN") - } - - model Person extends Entity { - studios Studio[] - owners User[] - clips Clip[] - events Event[] - galleries Gallery[] - } - - model Studio extends Entity { - people Person[] - owners User[] - clips Clip[] - events Event[] - galleries Gallery[] - } - - model Clip extends Entity { - url String? - people Person[] - studios Studio[] - galleries Gallery[] - } - - model UserRanking extends UserEntityRelation { - id String @id @default(cuid()) - rank Int @gte(1) @lte(100) - note String? @length(max: 300) - } - - model UserFavorite extends UserEntityRelation { - id String @id @default(cuid()) - favoritedAt DateTime @default(now()) - } - - model UserRating extends UserEntityRelation { - id String @id @default(cuid()) - rating Int @gte(1) @lte(5) - note String? @length(max: 500) - ratedAt DateTime @default(now()) - } - - model Event { - id Int @id @default(autoincrement()) - name String @length(max: 100) - desc String? @length(max: 500) - location String? @length(max: 100) - date DateTime? - people Person[] - studios Studio[] - - @@allow('read', true) - @@allow('create,update,delete', auth().role == "ADMIN") - } - - model Gallery { - id String @id @default(cuid()) - studioId String? - personId String? - timestamp DateTime @default(now()) - authorId String - author User @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: NoAction) - people Person[] - studios Studio[] - clips Clip[] - attachments Attachment[] - - @@allow('read', true) - @@allow('create,update,delete', auth().id == this.authorId && auth().role == "ADMIN") - } - `; - - await loadSchema(schema); - }); -}); diff --git a/tests/regression/tests/issue-1078.test.ts b/tests/regression/tests/issue-1078.test.ts deleted file mode 100644 index 3c0fc7024..000000000 --- a/tests/regression/tests/issue-1078.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1078', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Counter { - id String @id - - name String - value Int - - @@validate(value >= 0) - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await expect( - db.counter.create({ - data: { id: '1', name: 'It should create', value: 1 }, - }) - ).toResolveTruthy(); - - //! This query fails validation - await expect( - db.counter.update({ - where: { id: '1' }, - data: { name: 'It should update' }, - }) - ).toResolveTruthy(); - }); - - it('read', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Post { - id Int @id() @default(autoincrement()) - title String @allow('read', true, true) - content String - } - ` - ); - - const db = enhance(); - - const post = await prisma.post.create({ data: { title: 'Post1', content: 'Content' } }); - await expect(db.post.findUnique({ where: { id: post.id } })).toResolveNull(); - await expect(db.post.findUnique({ where: { id: post.id }, select: { title: true } })).resolves.toEqual({ - title: 'Post1', - }); - }); -}); diff --git a/tests/regression/tests/issue-1080.test.ts b/tests/regression/tests/issue-1080.test.ts deleted file mode 100644 index 69408fdf0..000000000 --- a/tests/regression/tests/issue-1080.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1080', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Project { - id String @id @unique @default(uuid()) - Fields Field[] - - @@allow('all', true) - } - - model Field { - id String @id @unique @default(uuid()) - name String - Project Project @relation(fields: [projectId], references: [id]) - projectId String - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - const project = await db.project.create({ - include: { Fields: true }, - data: { - Fields: { - create: [{ name: 'first' }, { name: 'second' }], - }, - }, - }); - - let updated = await db.project.update({ - where: { id: project.id }, - include: { Fields: true }, - data: { - Fields: { - upsert: [ - { - where: { id: project.Fields[0].id }, - create: { name: 'first1' }, - update: { name: 'first1' }, - }, - { - where: { id: project.Fields[1].id }, - create: { name: 'second1' }, - update: { name: 'second1' }, - }, - ], - }, - }, - }); - expect(updated).toMatchObject({ - Fields: expect.arrayContaining([ - expect.objectContaining({ name: 'first1' }), - expect.objectContaining({ name: 'second1' }), - ]), - }); - - updated = await db.project.update({ - where: { id: project.id }, - include: { Fields: true }, - data: { - Fields: { - upsert: { - where: { id: project.Fields[0].id }, - create: { name: 'first2' }, - update: { name: 'first2' }, - }, - }, - }, - }); - expect(updated).toMatchObject({ - Fields: expect.arrayContaining([ - expect.objectContaining({ name: 'first2' }), - expect.objectContaining({ name: 'second1' }), - ]), - }); - - updated = await db.project.update({ - where: { id: project.id }, - include: { Fields: true }, - data: { - Fields: { - upsert: { - where: { id: project.Fields[0].id }, - create: { name: 'first3' }, - update: { name: 'first3' }, - }, - update: { - where: { id: project.Fields[1].id }, - data: { name: 'second3' }, - }, - }, - }, - }); - expect(updated).toMatchObject({ - Fields: expect.arrayContaining([ - expect.objectContaining({ name: 'first3' }), - expect.objectContaining({ name: 'second3' }), - ]), - }); - - updated = await db.project.update({ - where: { id: project.id }, - include: { Fields: true }, - data: { - Fields: { - upsert: { - where: { id: 'non-exist' }, - create: { name: 'third1' }, - update: { name: 'third1' }, - }, - update: { - where: { id: project.Fields[1].id }, - data: { name: 'second4' }, - }, - }, - }, - }); - expect(updated).toMatchObject({ - Fields: expect.arrayContaining([ - expect.objectContaining({ name: 'first3' }), - expect.objectContaining({ name: 'second4' }), - expect.objectContaining({ name: 'third1' }), - ]), - }); - }); -}); diff --git a/tests/regression/tests/issue-1100.test.ts b/tests/regression/tests/issue-1100.test.ts deleted file mode 100644 index 8b1945b8d..000000000 --- a/tests/regression/tests/issue-1100.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { loadModelWithError, loadSchema } from '@zenstackhq/testtools'; - -describe('Regression for issue 1100', () => { - it('missing opposite relation', async () => { - const schema = ` - model User { - id String @id @default(cuid()) - name String? - content Content[] - post Post[] - } - - model Content { - id String @id @default(cuid()) - published Boolean @default(false) - contentType String - @@delegate(contentType) - - user User @relation(fields: [userId], references: [id]) - userId String - } - - model Post extends Content { - title String - } - - model Image extends Content { - url String - } - `; - - await expect(loadModelWithError(schema)).resolves.toContain( - 'The relation field "post" on model "User" is missing an opposite relation field on model "Post"' - ); - }); - - it('success', async () => { - const schema = ` - model User { - id String @id @default(cuid()) - name String? - content Content[] - post Post[] - } - - model Content { - id String @id @default(cuid()) - published Boolean @default(false) - contentType String - @@delegate(contentType) - - user User @relation(fields: [userId], references: [id]) - userId String - } - - model Post extends Content { - title String - author User @relation(fields: [authorId], references: [id]) - authorId String - } - - model Image extends Content { - url String - } - `; - - await expect(loadSchema(schema)).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1123.test.ts b/tests/regression/tests/issue-1123.test.ts deleted file mode 100644 index 02ee7a983..000000000 --- a/tests/regression/tests/issue-1123.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression for issue 1123', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Content { - id String @id @default(cuid()) - published Boolean @default(false) - contentType String - likes Like[] - @@delegate(contentType) - @@allow('all', true) - } - - model Post extends Content { - title String - } - - model Image extends Content { - url String - } - - model Like { - id String @id @default(cuid()) - content Content @relation(fields: [contentId], references: [id]) - contentId String - @@allow('all', true) - } - ` - ); - - const db = enhance(); - await db.post.create({ - data: { - title: 'a post', - likes: { create: {} }, - }, - }); - - await expect(db.content.findFirst({ include: { _count: { select: { likes: true } } } })).resolves.toMatchObject( - { - _count: { likes: 1 }, - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1129.test.ts b/tests/regression/tests/issue-1129.test.ts deleted file mode 100644 index 49198a5cb..000000000 --- a/tests/regression/tests/issue-1129.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('Regression for issue 1129', () => { - it('regression', async () => { - let prisma; - const dbUrl = await createPostgresDb('regression-issue-1129'); - - try { - const r = await loadSchema( - ` - model Relation1 { - id String @id @default(cuid()) - field1 String - concrete Concrete[] - @@allow('all', true) - } - - model Relation2 { - id String @id @default(cuid()) - field2 String - concrete Concrete[] - @@allow('all', true) - } - - abstract model WithRelation1 { - relation1Id String - relation1 Relation1 @relation(fields: [relation1Id], references: [id]) - } - abstract model WithRelation2 { - relation2Id String - relation2 Relation2 @relation(fields: [relation2Id], references: [id]) - } - - model Concrete extends WithRelation1, WithRelation2 { - concreteField String - @@id([relation1Id, relation2Id]) - @@allow('all', true) - } - `, - { provider: 'postgresql', dbUrl } - ); - - prisma = r.prisma; - const db = r.enhance(); - - await db.$transaction(async (tx: any) => { - await tx.relation2.createMany({ - data: [ - { - id: 'relation2Id1', - field2: 'field2Value1', - }, - { - id: 'relation2Id2', - field2: 'field2Value2', - }, - ], - }); - - await tx.relation1.create({ - data: { - field1: 'field1Value', - concrete: { - createMany: { - data: [ - { - concreteField: 'concreteFieldValue1', - relation2Id: 'relation2Id1', - }, - { - concreteField: 'concreteFieldValue2', - relation2Id: 'relation2Id2', - }, - ], - }, - }, - }, - }); - }); - } finally { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb('regression-issue-1129'); - } - }); -}); diff --git a/tests/regression/tests/issue-1135.test.ts b/tests/regression/tests/issue-1135.test.ts deleted file mode 100644 index 1497621d5..000000000 --- a/tests/regression/tests/issue-1135.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression for issue 1135', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Attachment { - id String @id @default(cuid()) - url String - myEntityId String - myEntity Entity @relation(fields: [myEntityId], references: [id], onUpdate: NoAction) - - @@allow('all', true) - } - - model Entity { - id String @id @default(cuid()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt @default(now()) - - attachments Attachment[] - - type String - @@delegate(type) - @@allow('all', true) - } - - model Person extends Entity { - age Int? - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { enhance } from '.zenstack/enhance'; -import { PrismaClient } from '@prisma/client'; - -const db = enhance(new PrismaClient()); - -db.person.create({ - data: { - name: 'test', - attachments: { - create: { - url: 'https://...', - }, - }, - }, -}); - `, - }, - ], - } - ); - - const db = enhance(); - await expect( - db.person.create({ - data: { - name: 'test', - attachments: { - create: { - url: 'https://...', - }, - }, - }, - include: { attachments: true }, - }) - ).resolves.toMatchObject({ - id: expect.any(String), - name: 'test', - attachments: [ - { - id: expect.any(String), - url: 'https://...', - myEntityId: expect.any(String), - }, - ], - }); - }); -}); diff --git a/tests/regression/tests/issue-1149.test.ts b/tests/regression/tests/issue-1149.test.ts deleted file mode 100644 index 3f3f43e85..000000000 --- a/tests/regression/tests/issue-1149.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('Regression for issue 1149', () => { - let prisma: any; - let dbUrl: string; - - beforeAll(async () => { - dbUrl = await createPostgresDb('issue-1149'); - }); - - afterAll(async () => { - if (prisma) { - await prisma.$disconnect(); - } - dropPostgresDb('issue-1149'); - }); - - it('test', async () => { - const schema = ` - model User { - id String @id @default(cuid()) - name String - - userRankings UserRanking[] - userFavorites UserFavorite[] - } - - model Entity { - id String @id @default(cuid()) - name String - type String - userRankings UserRanking[] - userFavorites UserFavorite[] - - @@delegate(type) - } - - model Person extends Entity { - } - - model Studio extends Entity { - } - - - model UserRanking { - id String @id @default(cuid()) - rank Int - - entityId String - entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) - } - - model UserFavorite { - id String @id @default(cuid()) - - entityId String - entity Entity @relation(fields: [entityId], references: [id], onUpdate: NoAction) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) - } - `; - - const { enhance, prisma: _prisma } = await loadSchema(schema, { - provider: 'postgresql', - dbUrl, - enhancements: ['delegate'], - }); - - prisma = _prisma; - const db = enhance(); - - const user = await db.user.create({ data: { name: 'user' } }); - const person = await db.person.create({ data: { name: 'person' } }); - - await expect( - db.userRanking.createMany({ - data: { - rank: 1, - entity: { connect: { id: person.id } }, - user: { connect: { id: user.id } }, - }, - }) - ).resolves.toMatchObject({ count: 1 }); - - await expect( - db.userRanking.createMany({ - data: [ - { - rank: 2, - entity: { connect: { id: person.id } }, - user: { connect: { id: user.id } }, - }, - { - rank: 3, - entity: { connect: { id: person.id } }, - user: { connect: { id: user.id } }, - }, - ], - }) - ).resolves.toMatchObject({ count: 2 }); - - await expect(db.userRanking.findMany()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ rank: 1 }), - expect.objectContaining({ rank: 2 }), - expect.objectContaining({ rank: 3 }), - ]) - ); - }); -}); diff --git a/tests/regression/tests/issue-1167.test.ts b/tests/regression/tests/issue-1167.test.ts deleted file mode 100644 index 29b81adbf..000000000 --- a/tests/regression/tests/issue-1167.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1167', () => { - it('regression', async () => { - await loadSchema( - ` - model FileAsset { - id String @id @default(cuid()) - delegate_type String - @@delegate(delegate_type) - @@map("file_assets") - } - - model ImageAsset extends FileAsset { - @@map("image_assets") - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1179.test.ts b/tests/regression/tests/issue-1179.test.ts deleted file mode 100644 index 3d5fd8d99..000000000 --- a/tests/regression/tests/issue-1179.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { loadModel } from '@zenstackhq/testtools'; - -describe('issue 1179', () => { - it('regression', async () => { - await loadModel( - ` - abstract model Base { - id String @id @default(uuid()) - } - - model User extends Base { - email String - posts Post[] - @@allow('all', auth() == this) - } - - model Post { - id String @id @default(uuid()) - - user User @relation(fields: [userId], references: [id]) - userId String - @@allow('all', auth().id == userId) - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1186.test.ts b/tests/regression/tests/issue-1186.test.ts deleted file mode 100644 index d36efcd57..000000000 --- a/tests/regression/tests/issue-1186.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1186', () => { - it('regression', async () => { - await loadSchema( - `schema.zmodel - import "model" - - ${FILE_SPLITTER}model.zmodel - generator client { - provider = "prisma-client-js" - binaryTargets = ["native"] - previewFeatures = ["postgresqlExtensions"] - } - - datasource db { - provider = "postgresql" - extensions = [citext] - - url = env("DATABASE_URL") - } - enum UserRole { - USER - ADMIN - } - - model User { - id String @id @default(uuid()) - role UserRole @default(USER) @deny('read,update', auth().role != ADMIN) - post Post[] - } - - abstract model Base { - id String @id @default(uuid()) - userId String - user User @relation(fields: [userId], references: [id]) - - @@allow('create', userId == auth().id) - @@allow('update', userId == auth().id && future().userId == auth().id) - - @@allow('all', auth().role == ADMIN) - } - - model Post extends Base { - description String - } - `, - { addPrelude: false, pushDb: false } - ); - }); -}); diff --git a/tests/regression/tests/issue-1210.test.ts b/tests/regression/tests/issue-1210.test.ts deleted file mode 100644 index ef1d407e1..000000000 --- a/tests/regression/tests/issue-1210.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1210', () => { - it('regression', async () => { - await loadSchema( - `schema.zmodel - import "./user" - import "./tokens" - - generator client { - provider = "prisma-client-js" - binaryTargets = ["native"] - previewFeatures = ["postgresqlExtensions"] - } - - datasource db { - provider = "postgresql" - extensions = [citext] - - url = env("DATABASE_URL") - } - - plugin zod { - provider = '@core/zod' - } - - ${FILE_SPLITTER}base.zmodel - abstract model Base { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? @omit - - @@deny('read', deletedAt != null) - @@deny('delete', true) - } - - ${FILE_SPLITTER}tokens.zmodel - import "base" - import "user" - - model Session extends Base { - expiresAt DateTime - userId String - user User @relation(references: [id], fields: [userId], onDelete: Cascade) - } - - ${FILE_SPLITTER}user.zmodel - import "base" - import "tokens" - enum UserRole { - USER - ADMIN - } - - model User extends Base { - email String @unique @db.Citext @email @trim @lower - role UserRole @default(USER) @deny('read,update', auth().role != ADMIN) - - sessions Session[] - posts Post[] - - @@allow('read,create', auth() == this) - @@allow('all', auth().role == ADMIN) - } - - abstract model UserEntity extends Base { - userId String - user User @relation(fields: [userId], references: [id]) - - @@allow('create', userId == auth().id) - @@allow('update', userId == auth().id && future().userId == auth().id) - - @@allow('all', auth().role == ADMIN) - } - - abstract model PrivateUserEntity extends UserEntity { - @@allow('read', userId == auth().id) - } - - abstract model PublicUserEntity extends UserEntity { - @@allow('read', true) - } - - model Post extends PublicUserEntity { - title String - } - `, - { addPrelude: false, pushDb: false } - ); - }); -}); diff --git a/tests/regression/tests/issue-1235.test.ts b/tests/regression/tests/issue-1235.test.ts deleted file mode 100644 index 1e9f80f86..000000000 --- a/tests/regression/tests/issue-1235.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1235', () => { - it('regression1', async () => { - const { enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - @@deny("update", future().id != id) - @@allow("all", true) - } - ` - ); - - const db = enhance(); - const post = await db.post.create({ data: {} }); - await expect(db.post.update({ data: { id: post.id + 1 }, where: { id: post.id } })).toBeRejectedByPolicy(); - }); - - it('regression2', async () => { - const { enhance } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - @@deny("update", future().id != this.id) - @@allow("all", true) - } - ` - ); - - const db = enhance(); - const post = await db.post.create({ data: {} }); - await expect(db.post.update({ data: { id: post.id + 1 }, where: { id: post.id } })).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/regression/tests/issue-1241.test.ts b/tests/regression/tests/issue-1241.test.ts deleted file mode 100644 index a555fcb8d..000000000 --- a/tests/regression/tests/issue-1241.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import { randomBytes } from 'crypto'; - -describe('issue 1241', () => { - it('regression', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - todos Todo[] - - @@auth - @@allow('all', true) - } - - model Todo { - id String @id @default(uuid()) - - user_id String - user User @relation(fields: [user_id], references: [id]) - - images File[] @relation("todo_images") - documents File[] @relation("todo_documents") - - @@allow('all', true) - } - - model File { - id String @id @default(uuid()) - s3_key String @unique - label String - - todo_image_id String? - todo_image Todo? @relation("todo_images", fields: [todo_image_id], references: [id]) - - todo_document_id String? - todo_document Todo? @relation("todo_documents", fields: [todo_document_id], references: [id]) - - @@allow('all', true) - } - ` - ); - - const user = await prisma.user.create({ - data: {}, - }); - await prisma.todo.create({ - data: { - user_id: user.id, - - images: { - create: new Array(3).fill(null).map((_, i) => ({ - s3_key: randomBytes(8).toString('hex'), - label: `img-label-${i + 1}`, - })), - }, - - documents: { - create: new Array(3).fill(null).map((_, i) => ({ - s3_key: randomBytes(8).toString('hex'), - label: `doc-label-${i + 1}`, - })), - }, - }, - }); - - const db = enhance(); - - const todo = await db.todo.findFirst({ where: {}, include: { documents: true } }); - await expect( - db.todo.update({ - where: { id: todo.id }, - data: { - documents: { - update: todo.documents.map((doc: any) => { - return { - where: { s3_key: doc.s3_key }, - data: { label: 'updated' }, - }; - }), - }, - }, - include: { documents: true }, - }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1243.test.ts b/tests/regression/tests/issue-1243.test.ts deleted file mode 100644 index 941bc9b61..000000000 --- a/tests/regression/tests/issue-1243.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression for issue 1243', () => { - it('uninheritable fields', async () => { - const schema = ` - model Base { - id String @id @default(cuid()) - type String - foo String - - @@delegate(type) - @@index([foo]) - @@map('base') - @@unique([foo]) - } - - model Item1 extends Base { - x String - } - - model Item2 extends Base { - y String - } - `; - - await loadSchema(schema, { - enhancements: ['delegate'], - }); - }); - - it('multiple id fields', async () => { - const schema = ` - model Base { - id1 String - id2 String - type String - - @@delegate(type) - @@id([id1, id2]) - } - - model Item1 extends Base { - x String - } - - model Item2 extends Base { - y String - } - `; - - await loadSchema(schema, { - enhancements: ['delegate'], - }); - }); -}); diff --git a/tests/regression/tests/issue-1257.test.ts b/tests/regression/tests/issue-1257.test.ts deleted file mode 100644 index a692d0464..000000000 --- a/tests/regression/tests/issue-1257.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1210', () => { - it('regression', async () => { - await loadSchema( - `schema.zmodel - import "./user" - import "./image" - - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - } - - ${FILE_SPLITTER}base.zmodel - abstract model Base { - id Int @id @default(autoincrement()) - } - - ${FILE_SPLITTER}user.zmodel - import "./base" - import "./image" - - enum Role { - Admin - } - - model User extends Base { - email String @unique - role Role - @@auth - } - - ${FILE_SPLITTER}image.zmodel - import "./user" - import "./base" - - model Image extends Base { - width Int @default(0) - height Int @default(0) - - @@allow('read', true) - @@allow('all', auth().role == Admin) - } - `, - { addPrelude: false, pushDb: false } - ); - }); -}); diff --git a/tests/regression/tests/issue-1265.test.ts b/tests/regression/tests/issue-1265.test.ts deleted file mode 100644 index cd7df4636..000000000 --- a/tests/regression/tests/issue-1265.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1265', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - posts Post[] - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String @default('xyz') - userId String @default(auth().id) - user User @relation(fields: [userId], references: [id]) - @@allow('all', true) - } - `, - { fullZod: true, pushDb: false } - ); - - expect(zodSchemas.models.PostCreateSchema.safeParse({ title: 'Post 1' }).success).toBeTruthy(); - expect(zodSchemas.input.PostInputSchema.create.safeParse({ data: { title: 'Post 1' } }).success).toBeTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1268.test.ts b/tests/regression/tests/issue-1268.test.ts deleted file mode 100644 index b51d954f7..000000000 --- a/tests/regression/tests/issue-1268.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1268', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` - model Model { - id String @id @default(uuid()) - bytes Bytes - } - `, - { - fullZod: true, - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'test.ts', - content: ` -import { ModelCreateInputObjectSchema } from '.zenstack/zod/objects'; -ModelCreateInputObjectSchema.parse({ bytes: new Uint8Array(0) }); - `, - }, - ], - } - ); - - expect( - zodSchemas.objects.ModelCreateInputObjectSchema.safeParse({ bytes: new Uint8Array(0) }).success - ).toBeTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1271.test.ts b/tests/regression/tests/issue-1271.test.ts deleted file mode 100644 index 9798664cb..000000000 --- a/tests/regression/tests/issue-1271.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1271', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(uuid()) - - @@auth - @@allow('all', true) - } - - model Test { - id String @id @default(uuid()) - linkingTable LinkingTable[] - key String @default('test') - locale String @default('EN') - - @@unique([key, locale]) - @@allow("all", true) - } - - model LinkingTable { - test_id String - test Test @relation(fields: [test_id], references: [id]) - - another_test_id String - another_test AnotherTest @relation(fields: [another_test_id], references: [id]) - - @@id([test_id, another_test_id]) - @@allow("all", true) - } - - model AnotherTest { - id String @id @default(uuid()) - status String - linkingTable LinkingTable[] - - @@allow("all", true) - } - ` - ); - - const db = enhance(); - - const test = await db.test.create({ - data: { - key: 'test1', - }, - }); - const anotherTest = await db.anotherTest.create({ - data: { - status: 'available', - }, - }); - - const updated = await db.test.upsert({ - where: { - key_locale: { - key: test.key, - locale: test.locale, - }, - }, - create: { - linkingTable: { - create: { - another_test_id: anotherTest.id, - }, - }, - }, - update: { - linkingTable: { - create: { - another_test_id: anotherTest.id, - }, - }, - }, - include: { - linkingTable: true, - }, - }); - - expect(updated.linkingTable).toHaveLength(1); - expect(updated.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); - - const test2 = await db.test.upsert({ - where: { - key_locale: { - key: 'test2', - locale: 'locale2', - }, - }, - create: { - key: 'test2', - locale: 'locale2', - linkingTable: { - create: { - another_test_id: anotherTest.id, - }, - }, - }, - update: { - linkingTable: { - create: { - another_test_id: anotherTest.id, - }, - }, - }, - include: { - linkingTable: true, - }, - }); - expect(test2).toMatchObject({ key: 'test2', locale: 'locale2' }); - expect(test2.linkingTable).toHaveLength(1); - expect(test2.linkingTable[0]).toMatchObject({ another_test_id: anotherTest.id }); - - const linkingTable = test2.linkingTable[0]; - - // connectOrCreate: connect case - const test3 = await db.test.create({ - data: { - key: 'test3', - locale: 'locale3', - }, - }); - console.log('test3 created:', test3); - const updated2 = await db.linkingTable.update({ - where: { - test_id_another_test_id: { - test_id: linkingTable.test_id, - another_test_id: linkingTable.another_test_id, - }, - }, - data: { - test: { - connectOrCreate: { - where: { - key_locale: { - key: test3.key, - locale: test3.locale, - }, - }, - create: { - key: 'test4', - locale: 'locale4', - }, - }, - }, - another_test: { connect: { id: anotherTest.id } }, - }, - include: { test: true }, - }); - expect(updated2).toMatchObject({ - test: expect.objectContaining({ key: 'test3', locale: 'locale3' }), - another_test_id: anotherTest.id, - }); - - // connectOrCreate: create case - const updated3 = await db.linkingTable.update({ - where: { - test_id_another_test_id: { - test_id: updated2.test_id, - another_test_id: updated2.another_test_id, - }, - }, - data: { - test: { - connectOrCreate: { - where: { - key_locale: { - key: 'test4', - locale: 'locale4', - }, - }, - create: { - key: 'test4', - locale: 'locale4', - }, - }, - }, - another_test: { connect: { id: anotherTest.id } }, - }, - include: { test: true }, - }); - expect(updated3).toMatchObject({ - test: expect.objectContaining({ key: 'test4', locale: 'locale4' }), - another_test_id: anotherTest.id, - }); - }); -}); diff --git a/tests/regression/tests/issue-1378.test.ts b/tests/regression/tests/issue-1378.test.ts deleted file mode 100644 index 5dd5b8e15..000000000 --- a/tests/regression/tests/issue-1378.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1378', () => { - it('regression', async () => { - await loadSchema( - ` - model User { - id String @id @default(cuid()) - todos Todo[] - } - - model Todo { - id String @id @default(cuid()) - name String @length(3,255) - userId String @default(auth().id) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@allow("all", auth() == user) - } - `, - { - extraDependencies: ['zod'], - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { z } from 'zod/v3'; - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - import { TodoCreateSchema } from '.zenstack/zod/models'; - - const prisma = new PrismaClient(); - const db = enhance(prisma); - - export const onSubmit = async (values: z.infer) => { - await db.todo.create({ - data: values, - }); - }; - `, - }, - ], - compile: true, - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1381.test.ts b/tests/regression/tests/issue-1381.test.ts deleted file mode 100644 index 14bf8f4b9..000000000 --- a/tests/regression/tests/issue-1381.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1381', () => { - it('regression', async () => { - await loadSchema( - ` - enum MemberRole { - owner - admin - } - - enum SpaceType { - contractor - public - private - } - - model User { - id String @id @default(cuid()) - name String? - email String? @unique @lower - password String? @password @omit - memberships Membership[] - } - - model Membership { - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - spaceId String - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - role MemberRole @deny("update", auth() == user) - @@id([userId, spaceId]) - } - - model Space { - id String @id @default(cuid()) - name String - type SpaceType @default(private) - memberships Membership[] - options Option[] - } - - model Option { - id String @id @default(cuid()) - name String? - spaceId String? - space Space? @relation(fields: [spaceId], references: [id], onDelete: SetNull) - - @@allow("update", - future().space.type in [contractor, public] && - future().space.memberships?[space.type in [contractor, public] && auth() == user && role in [owner, admin]] - ) - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1388.test.ts b/tests/regression/tests/issue-1388.test.ts deleted file mode 100644 index 3ffbc967b..000000000 --- a/tests/regression/tests/issue-1388.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1388', () => { - it('regression', async () => { - await loadSchema( - `schema.zmodel - import './auth' - import './post' - - ${FILE_SPLITTER}auth.zmodel - model User { - id String @id @default(cuid()) - role String - } - - ${FILE_SPLITTER}post.zmodel - model Post { - id String @id @default(nanoid(6)) - title String - @@deny('all', auth() == null) - @@allow('all', auth().id == 'user1') - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1410.test.ts b/tests/regression/tests/issue-1410.test.ts deleted file mode 100644 index 488cd1bf0..000000000 --- a/tests/regression/tests/issue-1410.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1410', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Drink { - id Int @id @default(autoincrement()) - slug String @unique - - manufacturer_id Int - manufacturer Manufacturer @relation(fields: [manufacturer_id], references: [id]) - - type String - - name String @unique - description String - abv Float - image String? - - gluten Boolean - lactose Boolean - organic Boolean - - containers Container[] - - @@delegate(type) - - @@allow('all', true) - } - - model Beer extends Drink { - style_id Int - style BeerStyle @relation(fields: [style_id], references: [id]) - - ibu Float? - - @@allow('all', true) - } - - model BeerStyle { - id Int @id @default(autoincrement()) - - name String @unique - color String - - beers Beer[] - - @@allow('all', true) - } - - model Wine extends Drink { - style_id Int - style WineStyle @relation(fields: [style_id], references: [id]) - - heavy_score Int? - tannine_score Int? - dry_score Int? - fresh_score Int? - notes String? - - @@allow('all', true) - } - - model WineStyle { - id Int @id @default(autoincrement()) - - name String @unique - color String - - wines Wine[] - - @@allow('all', true) - } - - model Soda extends Drink { - carbonated Boolean - - @@allow('all', true) - } - - model Cocktail extends Drink { - mix Boolean - - @@allow('all', true) - } - - model Container { - barcode String @id - - drink_id Int - drink Drink @relation(fields: [drink_id], references: [id]) - - type String - volume Int - portions Int? - - inventory Int @default(0) - - @@allow('all', true) - } - - model Manufacturer { - id Int @id @default(autoincrement()) - - country_id String - country Country @relation(fields: [country_id], references: [code]) - - name String @unique - description String? - image String? - - drinks Drink[] - - @@allow('all', true) - } - - model Country { - code String @id - name String - - manufacturers Manufacturer[] - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await db.beer.findMany({ - include: { style: true, manufacturer: true }, - where: { NOT: { gluten: true } }, - }); - - await db.beer.findMany({ - include: { style: true, manufacturer: true }, - where: { AND: [{ gluten: true }, { abv: { gt: 50 } }] }, - }); - - await db.beer.findMany({ - include: { style: true, manufacturer: true }, - where: { OR: [{ AND: [{ NOT: { gluten: true } }] }, { abv: { gt: 50 } }] }, - }); - }); -}); diff --git a/tests/regression/tests/issue-1415.test.ts b/tests/regression/tests/issue-1415.test.ts deleted file mode 100644 index 791413557..000000000 --- a/tests/regression/tests/issue-1415.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1415', () => { - it('regression', async () => { - await loadSchema( - ` - model User { - id String @id @default(cuid()) - prices Price[] - } - - model Price { - id String @id @default(cuid()) - owner User @relation(fields: [ownerId], references: [id]) - ownerId String @default(auth().id) - priceType String - @@delegate(priceType) - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1416.test.ts b/tests/regression/tests/issue-1416.test.ts deleted file mode 100644 index 5c18d6d4e..000000000 --- a/tests/regression/tests/issue-1416.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1416', () => { - it('regression', async () => { - await loadSchema( - ` - model User { - id String @id @default(cuid()) - role String - } - - model Price { - id String @id @default(nanoid(6)) - entity Entity? @relation(fields: [entityId], references: [id]) - entityId String? - priceType String - @@delegate(priceType) - } - - model MyPrice extends Price { - foo String - } - - model Entity { - id String @id @default(nanoid(6)) - price Price[] - type String - @@delegate(type) - } - - model MyEntity extends Entity { - foo String - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1427.test.ts b/tests/regression/tests/issue-1427.test.ts deleted file mode 100644 index 0d7c7c07e..000000000 --- a/tests/regression/tests/issue-1427.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1427', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - name String - profile Profile? - @@allow('all', true) - } - - model Profile { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String @unique - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { - name: 'John', - profile: { - create: {}, - }, - }, - }); - - const db = enhance(); - const found = await db.user.findFirst({ - select: { - id: true, - name: true, - profile: false, - }, - }); - expect(found.profile).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-1435.test.ts b/tests/regression/tests/issue-1435.test.ts deleted file mode 100644 index d539b778f..000000000 --- a/tests/regression/tests/issue-1435.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1435', () => { - it('regression', async () => { - let prisma: any; - let dbUrl: string; - - try { - dbUrl = await createPostgresDb('issue-1435'); - const r = await loadSchema( - ` - /* Interfaces */ - abstract model IBase { - updatedAt DateTime @updatedAt - createdAt DateTime @default(now()) - } - - abstract model IAuth extends IBase { - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String @unique - - @@allow('create', true) - @@allow('all', auth() == user) - } - - abstract model IIntegration extends IBase { - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - organizationId String @unique - - @@allow('all', organization.members?[user == auth() && type == OWNER]) - @@allow('read', organization.members?[user == auth()]) - } - - /* Auth Stuff */ - model User extends IBase { - id String @id @default(cuid()) - firstName String - lastName String - google GoogleAuth? - memberships Member[] - - @@allow('create', true) - @@allow('all', auth() == this) - } - - model GoogleAuth extends IAuth { - reference String @id - refreshToken String - } - - /* Org Stuff */ - enum MemberType { - OWNER - MEMBER - } - - model Organization extends IBase { - id String @id @default(cuid()) - name String - members Member[] - google GoogleIntegration? - - @@allow('create', true) - @@allow('all', members?[user == auth() && type == OWNER]) - @@allow('read', members?[user == auth()]) - } - - - model Member extends IBase { - type MemberType @default(MEMBER) - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - organizationId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - - @@id([organizationId, userId]) - @@allow('all', organization.members?[user == auth() && type == OWNER]) - @@allow('read', user == auth()) - } - - /* Google Stuff */ - model GoogleIntegration extends IIntegration { - reference String @id - } - `, - { provider: 'postgresql', dbUrl } - ); - - prisma = r.prisma; - const enhance = r.enhance; - - await prisma.organization.create({ - data: { - name: 'My Organization', - members: { - create: { - type: 'OWNER', - user: { - create: { - id: '1', - firstName: 'John', - lastName: 'Doe', - }, - }, - }, - }, - }, - }); - - const db = enhance({ id: '1' }); - await expect(db.organization.findMany()).resolves.toHaveLength(1); - } finally { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb('issue-1435'); - } - }); -}); diff --git a/tests/regression/tests/issue-1451.test.ts b/tests/regression/tests/issue-1451.test.ts deleted file mode 100644 index fb105561d..000000000 --- a/tests/regression/tests/issue-1451.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1452', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id String @id - memberships Membership[] - } - - model Space { - id String @id - memberships Membership[] - } - - model Membership { - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - spaceId String - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - - role String @deny("update", auth() == user) - employeeReference String? @deny("read, update", space.memberships?[auth() == user && !(role in ['owner', 'admin'])]) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@id([userId, spaceId]) - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { id: '1' }, - }); - - await prisma.space.create({ - data: { id: '1' }, - }); - - await prisma.membership.create({ - data: { - user: { connect: { id: '1' } }, - space: { connect: { id: '1' } }, - role: 'foo', - employeeReference: 'xyz', - }, - }); - - const db = enhance({ id: '1' }); - const r = await db.membership.findMany(); - expect(r).toHaveLength(1); - expect(r[0].employeeReference).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-1454.test.ts b/tests/regression/tests/issue-1454.test.ts deleted file mode 100644 index 6c42fcf59..000000000 --- a/tests/regression/tests/issue-1454.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1454', () => { - it('regression1', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - sensitiveInformation String - username String - - purchases Purchase[] - - @@allow('read', auth() == this) - } - - model Purchase { - id Int @id @default(autoincrement()) - purchasedAt DateTime @default(now()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@allow('read', true) - } - ` - ); - - const db = enhance(); - await prisma.user.create({ - data: { username: 'user1', sensitiveInformation: 'sensitive', purchases: { create: {} } }, - }); - - await expect(db.purchase.findMany({ where: { user: { username: 'user1' } } })).resolves.toHaveLength(0); - await expect(db.purchase.findMany({ where: { user: { is: { username: 'user1' } } } })).resolves.toHaveLength(0); - }); - - it('regression2', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - username String @allow('read', false) - - purchases Purchase[] - - @@allow('read', true) - } - - model Purchase { - id Int @id @default(autoincrement()) - purchasedAt DateTime @default(now()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@allow('read', true) - } - `, - { logPrismaQuery: true } - ); - - const db = enhance(); - const user = await prisma.user.create({ - data: { username: 'user1', purchases: { create: {} } }, - }); - - await expect(db.purchase.findMany({ where: { user: { id: user.id } } })).resolves.toHaveLength(1); - await expect(db.purchase.findMany({ where: { user: { username: 'user1' } } })).resolves.toHaveLength(0); - await expect(db.purchase.findMany({ where: { user: { is: { username: 'user1' } } } })).resolves.toHaveLength(0); - }); - - it('regression3', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - sensitiveInformation String - username String @allow('read', true, true) - - purchases Purchase[] - - @@allow('read', auth() == this) - } - - model Purchase { - id Int @id @default(autoincrement()) - purchasedAt DateTime @default(now()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@allow('read', true) - } - `, - { logPrismaQuery: true } - ); - - const db = enhance(); - await prisma.user.create({ - data: { username: 'user1', sensitiveInformation: 'sensitive', purchases: { create: {} } }, - }); - - await expect(db.purchase.findMany({ where: { user: { username: 'user1' } } })).resolves.toHaveLength(1); - await expect(db.purchase.findMany({ where: { user: { is: { username: 'user1' } } } })).resolves.toHaveLength(1); - await expect( - db.purchase.findMany({ where: { user: { sensitiveInformation: 'sensitive' } } }) - ).resolves.toHaveLength(0); - await expect( - db.purchase.findMany({ where: { user: { is: { sensitiveInformation: 'sensitive' } } } }) - ).resolves.toHaveLength(0); - await expect( - db.purchase.findMany({ where: { user: { username: 'user1', sensitiveInformation: 'sensitive' } } }) - ).resolves.toHaveLength(0); - await expect( - db.purchase.findMany({ - where: { OR: [{ user: { username: 'user1' } }, { user: { sensitiveInformation: 'sensitive' } }] }, - }) - ).resolves.toHaveLength(1); - }); -}); diff --git a/tests/regression/tests/issue-1466.test.ts b/tests/regression/tests/issue-1466.test.ts deleted file mode 100644 index f2526c85b..000000000 --- a/tests/regression/tests/issue-1466.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1466', () => { - it('regression1', async () => { - const dbUrl = await createPostgresDb('issue-1466-1'); - let prisma: any; - - try { - const r = await loadSchema( - ` - model UserLongLongLongLongLongLongLongLongName { - id Int @id @default(autoincrement()) - level Int @default(0) - asset AssetLongLongLongLongLongLongLongLongName @relation(fields: [assetId], references: [id]) - assetId Int @unique - } - - model AssetLongLongLongLongLongLongLongLongName { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - viewCount Int @default(0) - owner UserLongLongLongLongLongLongLongLongName? - assetType String - - @@delegate(assetType) - } - - model VideoLongLongLongLongLongLongLongLongName extends AssetLongLongLongLongLongLongLongLongName { - duration Int - } - `, - { - provider: 'postgresql', - dbUrl, - enhancements: ['delegate'], - } - ); - - prisma = r.prisma; - const db = r.enhance(); - - const video = await db.VideoLongLongLongLongLongLongLongLongName.create({ - data: { duration: 100 }, - }); - - const user = await db.UserLongLongLongLongLongLongLongLongName.create({ - data: { - asset: { connect: { id: video.id } }, - }, - }); - - const userWithAsset = await db.UserLongLongLongLongLongLongLongLongName.findFirst({ - include: { asset: true }, - }); - - expect(userWithAsset).toMatchObject({ - asset: { assetType: 'VideoLongLongLongLongLongLongLongLongName', duration: 100 }, - }); - } finally { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb('issue-1466-1'); - } - }); - - it('regression2', async () => { - const dbUrl = await createPostgresDb('issue-1466-2'); - let prisma: any; - - try { - const r = await loadSchema( - ` - model UserLongLongLongLongName { - id Int @id @default(autoincrement()) - level Int @default(0) - asset AssetLongLongLongLongName @relation(fields: [assetId], references: [id]) - assetId Int - - @@unique([assetId]) - } - - model AssetLongLongLongLongName { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - viewCount Int @default(0) - owner UserLongLongLongLongName? - assetType String - - @@delegate(assetType) - } - - model VideoLongLongLongLongName extends AssetLongLongLongLongName { - duration Int - } - `, - { - provider: 'postgresql', - dbUrl, - enhancements: ['delegate'], - } - ); - - prisma = r.prisma; - const db = r.enhance(); - - const video = await db.VideoLongLongLongLongName.create({ - data: { duration: 100 }, - }); - - const user = await db.UserLongLongLongLongName.create({ - data: { - asset: { connect: { id: video.id } }, - }, - }); - - const userWithAsset = await db.UserLongLongLongLongName.findFirst({ - include: { asset: true }, - }); - - expect(userWithAsset).toMatchObject({ - asset: { assetType: 'VideoLongLongLongLongName', duration: 100 }, - }); - } finally { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb('issue-1466-2'); - } - }); - - it('regression3', async () => { - await loadSchema( - ` - model UserLongLongLongLongName { - id Int @id @default(autoincrement()) - level Int @default(0) - asset AssetLongLongLongLongName @relation(fields: [assetId], references: [id]) - assetId Int @unique - } - - model AssetLongLongLongLongName { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - viewCount Int @default(0) - owner UserLongLongLongLongName? - assetType String - - @@delegate(assetType) - } - - model VideoLongLongLongLongName1 extends AssetLongLongLongLongName { - duration Int - } - - model VideoLongLongLongLongName2 extends AssetLongLongLongLongName { - format String - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - }); - - it('regression4', async () => { - await loadSchema( - ` - model UserLongLongLongLongName { - id Int @id @default(autoincrement()) - level Int @default(0) - asset AssetLongLongLongLongName @relation(fields: [assetId], references: [id]) - assetId Int @unique - } - - model AssetLongLongLongLongName { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - viewCount Int @default(0) - owner UserLongLongLongLongName? - assetType String - - @@delegate(assetType) - } - - model VideoLongLongLongLongName1 extends AssetLongLongLongLongName { - duration Int - } - - model VideoLongLongLongLongName2 extends AssetLongLongLongLongName { - format String - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - }); - - it('regression5', async () => { - await loadSchema( - ` - model UserLongLongLongLongName { - id Int @id @default(autoincrement()) - level Int @default(0) - asset AssetLongLongLongLongName @relation(fields: [assetId], references: [id]) - assetId Int @unique(map: 'assetId_unique') - } - - model AssetLongLongLongLongName { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - viewCount Int @default(0) - owner UserLongLongLongLongName? - assetType String - - @@delegate(assetType) - } - - model VideoLongLongLongLongName1 extends AssetLongLongLongLongName { - duration Int - } - - model VideoLongLongLongLongName2 extends AssetLongLongLongLongName { - format String - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1467.test.ts b/tests/regression/tests/issue-1467.test.ts deleted file mode 100644 index 374313e45..000000000 --- a/tests/regression/tests/issue-1467.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1467', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - type String - @@allow('all', true) - } - - model Container { - id Int @id @default(autoincrement()) - drink Drink @relation(fields: [drinkId], references: [id]) - drinkId Int - @@allow('all', true) - } - - model Drink { - id Int @id @default(autoincrement()) - name String @unique - containers Container[] - type String - - @@delegate(type) - @@allow('all', true) - } - - model Beer extends Drink { - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - await db.beer.create({ - data: { id: 1, name: 'Beer1' }, - }); - - await db.container.create({ data: { drink: { connect: { id: 1 } } } }); - await db.container.create({ data: { drink: { connect: { id: 1 } } } }); - - const beers = await db.beer.findFirst({ - select: { id: true, name: true, _count: { select: { containers: true } } }, - orderBy: { name: 'asc' }, - }); - expect(beers).toMatchObject({ _count: { containers: 2 } }); - }); -}); diff --git a/tests/regression/tests/issue-1474.test.ts b/tests/regression/tests/issue-1474.test.ts deleted file mode 100644 index 9e157d40d..000000000 --- a/tests/regression/tests/issue-1474.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1474', () => { - it('regression', async () => { - await loadSchema( - ` - model A { - id Int @id - cs C[] - } - - abstract model B { - a A @relation(fields: [aId], references: [id]) - aId Int - } - - model C extends B { - id Int @id - type String - @@delegate(type) - } - - model D extends C { - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1483.test.ts b/tests/regression/tests/issue-1483.test.ts deleted file mode 100644 index ee947ae2c..000000000 --- a/tests/regression/tests/issue-1483.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1483', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - @@auth - id String @id - edits Edit[] - @@allow('all', true) - } - - model Entity { - - id String @id @default(cuid()) - name String - edits Edit[] - - type String - @@delegate(type) - - @@allow('all', true) - } - - model Person extends Entity { - } - - model Edit { - id String @id @default(cuid()) - - authorId String? - author User? @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: NoAction) - - entityId String - entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade, onUpdate: NoAction) - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - await db.edit.deleteMany({}); - await db.person.deleteMany({}); - await db.user.deleteMany({}); - - const person = await db.person.create({ - data: { - name: 'test', - }, - }); - - await db.edit.create({ - data: { - entityId: person.id, - }, - }); - - await expect( - db.edit.findMany({ - include: { - author: true, - entity: true, - }, - }) - ).resolves.toHaveLength(1); - }); -}); diff --git a/tests/regression/tests/issue-1487.test.ts b/tests/regression/tests/issue-1487.test.ts deleted file mode 100644 index 6acfcdcfe..000000000 --- a/tests/regression/tests/issue-1487.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; -import Decimal from 'decimal.js'; - -describe('issue 1487', () => { - it('regression2', async () => { - const dbUrl = await createPostgresDb('issue-1487'); - let prisma: any; - - try { - const r = await loadSchema( - ` - model LineItem { - id Int @id @default(autoincrement()) - price Decimal - createdAt DateTime @default(now()) - - orderId Int - order Order @relation(fields: [orderId], references: [id]) - } - model Order extends BaseType { - total Decimal - createdAt DateTime @default(now()) - lineItems LineItem[] - } - model BaseType { - id Int @id @default(autoincrement()) - entityType String - - @@delegate(entityType) - } - `, - { - provider: 'postgresql', - dbUrl, - enhancements: ['omit', 'delegate'], - } - ); - - prisma = r.prisma; - const db = r.enhance(); - - const create = await db.Order.create({ - data: { - total: new Decimal(100_100.99), - lineItems: { create: [{ price: 90_000.66 }, { price: 20_100.33 }] }, - }, - }); - - const order = await db.Order.findFirst({ where: { id: create.id }, include: { lineItems: true } }); - expect(Decimal.isDecimal(order.total)).toBe(true); - expect(order.createdAt instanceof Date).toBe(true); - expect(order.total.toString()).toEqual('100100.99'); - order.lineItems.forEach((item: any) => { - expect(Decimal.isDecimal(item.price)).toBe(true); - expect(item.price.toString()).not.toEqual('[object Object]'); - }); - - const lineItems = await db.LineItem.findMany(); - lineItems.forEach((item: any) => { - expect(item.createdAt instanceof Date).toBe(true); - expect(Decimal.isDecimal(item.price)).toBe(true); - expect(item.price.toString()).not.toEqual('[object Object]'); - }); - } finally { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb('issue-1487'); - } - }); -}); diff --git a/tests/regression/tests/issue-1493.test.ts b/tests/regression/tests/issue-1493.test.ts deleted file mode 100644 index aa1bbb3af..000000000 --- a/tests/regression/tests/issue-1493.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1493', () => { - it('regression', async () => { - await loadSchema( - ` - datasource db { - provider = 'sqlite' - url = 'file:./dev.db' - } - - generator js { - provider = 'prisma-client-js' - } - - plugin enhancer { - provider = '@core/enhancer' - output = './zenstack' - } - - model User { - id Int @id - email String - posts Post[] - } - - model Post { - id Int @id - title String - content String - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) - } - `, - { - addPrelude: false, - compile: true, - getPrismaOnly: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from './zenstack/enhance'; - const prisma = new PrismaClient().$extends({ - result: { - user: { - gravatarUrl: { - needs: { email: true }, - compute(user) { - return user.email + 'hash'; - }, - }, - }, - }, - }); - - prisma.user.findFirst().then((user) => user?.gravatarUrl); - const db = enhance(prisma, undefined, { kinds: [] }); - db.user.findFirst().then((user) => user?.gravatarUrl); - `, - }, - ], - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1506.test.ts b/tests/regression/tests/issue-1506.test.ts deleted file mode 100644 index e8154c676..000000000 --- a/tests/regression/tests/issue-1506.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { loadModelWithError } from '@zenstackhq/testtools'; -describe('issue 1506', () => { - it('regression', async () => { - await expect( - loadModelWithError( - ` - model A { - id Int @id @default(autoincrement()) - value Int - b B @relation(fields: [bId], references: [id]) - bId Int @unique - - @@allow('read', true) - } - - model B { - id Int @id @default(autoincrement()) - value Int - a A? - c C @relation(fields: [cId], references: [id]) - cId Int @unique - - @@allow('read', value > c.value) - } - - model C { - id Int @id @default(autoincrement()) - value Int - b B? - - @@allow('read', true) - } - ` - ) - ).resolves.toContain( - 'comparison between fields of different models is not supported in model-level "read" rules' - ); - }); -}); diff --git a/tests/regression/tests/issue-1507.test.ts b/tests/regression/tests/issue-1507.test.ts deleted file mode 100644 index 49a6ee01d..000000000 --- a/tests/regression/tests/issue-1507.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1507', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - age Int - } - - model Profile { - id Int @id @default(autoincrement()) - age Int - - @@allow('read', auth().age == age) - } - `, - { preserveTsFiles: true, logPrismaQuery: true } - ); - - await prisma.profile.create({ data: { age: 18 } }); - await prisma.profile.create({ data: { age: 20 } }); - const db = enhance({ id: 1, age: 18 }); - await expect(db.profile.findMany()).resolves.toHaveLength(1); - await expect(db.profile.count()).resolves.toBe(1); - }); -}); diff --git a/tests/regression/tests/issue-1518.test.ts b/tests/regression/tests/issue-1518.test.ts deleted file mode 100644 index 83517a5ca..000000000 --- a/tests/regression/tests/issue-1518.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1518', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Activity { - id String @id @default(uuid()) - title String - type String - @@delegate(type) - @@allow('all', true) - } - - model TaskActivity extends Activity { - description String - @@map("task_activity") - @@allow('all', true) - } - ` - ); - - const db = enhance(); - await db.taskActivity.create({ - data: { - id: '00000000-0000-0000-0000-111111111111', - title: 'Test Activity', - description: 'Description of task', - }, - }); - }); -}); diff --git a/tests/regression/tests/issue-1520.test.ts b/tests/regression/tests/issue-1520.test.ts deleted file mode 100644 index 02ee1318c..000000000 --- a/tests/regression/tests/issue-1520.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1520', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Course { - id Int @id @default(autoincrement()) - title String - addedToNotifications AddedToCourseNotification[] - } - - model Group { - id Int @id @default(autoincrement()) - addedToNotifications AddedToGroupNotification[] - } - - model Notification { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - type String - senderId Int - receiverId Int - @@delegate (type) - } - - model AddedToGroupNotification extends Notification { - groupId Int - group Group @relation(fields: [groupId], references: [id], onDelete: Cascade) - } - - model AddedToCourseNotification extends Notification { - courseId Int - course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const r = await db.course.create({ - data: { - title: 'English classes', - addedToNotifications: { - createMany: { - data: [ - { - id: 1, - receiverId: 1, - senderId: 2, - }, - ], - }, - }, - }, - include: { addedToNotifications: true }, - }); - - expect(r.addedToNotifications).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: 1, - courseId: 1, - receiverId: 1, - senderId: 2, - }), - ]) - ); - }); -}); diff --git a/tests/regression/tests/issue-1522.test.ts b/tests/regression/tests/issue-1522.test.ts deleted file mode 100644 index bc6a9fb34..000000000 --- a/tests/regression/tests/issue-1522.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1522', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Course { - id String @id @default(uuid()) - title String - description String - sections Section[] - activities Activity[] - @@allow('all', true) - } - - model Section { - id String @id @default(uuid()) - title String - courseId String - idx Int @default(0) - course Course @relation(fields: [courseId], references: [id]) - activities Activity[] - } - - model Activity { - id String @id @default(uuid()) - title String - courseId String - sectionId String - idx Int @default(0) - type String - course Course @relation(fields: [courseId], references: [id]) - section Section @relation(fields: [sectionId], references: [id]) - @@delegate(type) - } - - model UrlActivity extends Activity { - url String - } - - model TaskActivity extends Activity { - description String - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const course = await db.course.create({ - data: { - title: 'Test Course', - description: 'Description of course', - sections: { - create: { - id: '00000000-0000-0000-0000-000000000002', - title: 'Test Section', - idx: 0, - }, - }, - }, - include: { - sections: true, - }, - }); - - const section = course.sections[0]; - await db.taskActivity.create({ - data: { - title: 'Test Activity', - description: 'Description of task', - idx: 0, - courseId: course.id, - sectionId: section.id, - }, - }); - - const found = await db.course.findFirst({ - where: { id: course.id }, - include: { - sections: { - orderBy: { idx: 'asc' }, - include: { - activities: { orderBy: { idx: 'asc' } }, - }, - }, - }, - }); - - expect(found.sections[0].activities[0]).toMatchObject({ - description: 'Description of task', - }); - }); -}); diff --git a/tests/regression/tests/issue-1530.test.ts b/tests/regression/tests/issue-1530.test.ts deleted file mode 100644 index 105de791c..000000000 --- a/tests/regression/tests/issue-1530.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1530', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Category { - id Int @id @default(autoincrement()) - name String @unique - - parentId Int? - parent Category? @relation("ParentChildren", fields: [parentId], references: [id]) - children Category[] @relation("ParentChildren") - @@allow('all', true) - } - ` - ); - - await prisma.category.create({ - data: { id: 1, name: 'C1' }, - }); - - const db = enhance(); - - await db.category.update({ - where: { id: 1 }, - data: { parent: { connect: { id: 1 } } }, - }); - - const r = await db.category.update({ - where: { id: 1 }, - data: { parent: { disconnect: true } }, - }); - expect(r.parent).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-1533.test.ts b/tests/regression/tests/issue-1533.test.ts deleted file mode 100644 index 611011fc1..000000000 --- a/tests/regression/tests/issue-1533.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; -describe('issue 1533', () => { - it('regression', async () => { - const dbUrl = await createPostgresDb('issue-1533'); - let prisma; - - try { - const r = await loadSchema( - ` - model Test { - id String @id @default(uuid()) @db.Uuid - metadata Json - @@allow('all', true) - } - `, - { provider: 'postgresql', dbUrl } - ); - - prisma = r.prisma; - const db = r.enhance(); - const Prisma = r.prismaModule; - - const testWithMetadata = await prisma.test.create({ - data: { - metadata: { - test: 'test', - }, - }, - }); - const testWithEmptyMetadata = await prisma.test.create({ - data: { - metadata: {}, - }, - }); - - let result = await db.test.findMany({ - where: { - metadata: { - path: ['test'], - equals: Prisma.DbNull, - }, - }, - }); - - expect(result).toHaveLength(1); - expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ id: testWithEmptyMetadata.id })])); - - result = await db.test.findMany({ - where: { - metadata: { - path: ['test'], - equals: 'test', - }, - }, - }); - - expect(result).toHaveLength(1); - expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ id: testWithMetadata.id })])); - } finally { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb('issue-1533'); - } - }); -}); diff --git a/tests/regression/tests/issue-1551.test.ts b/tests/regression/tests/issue-1551.test.ts deleted file mode 100644 index 3a330de24..000000000 --- a/tests/regression/tests/issue-1551.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1551', () => { - it('regression', async () => { - await loadSchema( - ` - model User { - id Int @id - profile Profile? @relation(fields: [profileId], references: [id]) - profileId Int? @unique @map('profile_id') - } - - model Profile { - id Int @id - contentType String - user User? - - @@delegate(contentType) - } - - model IndividualProfile extends Profile { - name String - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1560.test.ts b/tests/regression/tests/issue-1560.test.ts deleted file mode 100644 index 0652c2a90..000000000 --- a/tests/regression/tests/issue-1560.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1560', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` -model User { - id String @id @default(cuid()) - name String - ownedItems OwnedItem[] -} - -abstract model Base { - id String @id @default(cuid()) - ownerId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) -} - -model OwnedItem extends Base { - ownedItemType String - @@delegate(ownedItemType) -} - -model List extends OwnedItem { - title String -} - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - await db.user.create({ data: { id: '1', name: 'user1' } }); - await expect( - db.list.create({ data: { id: '1', title: 'list1', owner: { connect: { id: '1' } } } }) - ).resolves.toMatchObject({ - id: '1', - title: 'list1', - ownerId: '1', - ownedItemType: 'List', - }); - }); -}); diff --git a/tests/regression/tests/issue-1562.test.ts b/tests/regression/tests/issue-1562.test.ts deleted file mode 100644 index c07fee0d6..000000000 --- a/tests/regression/tests/issue-1562.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1562', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -plugin zod { - provider = '@core/zod' -} - -plugin enhancer { - provider = '@core/enhancer' - generatePermissionChecker = true -} - -abstract model Base { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - - // require login - @@allow('all', true) -} - -model User extends Base { - name String @unique @regex('^[a-zA-Z0-9_]{3,30}$') - - @@allow('read', true) -} - `, - { addPrelude: false } - ); - - const db = enhance(); - await expect(db.user.create({ data: { name: '1 2 3 4' } })).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/regression/tests/issue-1563.test.ts b/tests/regression/tests/issue-1563.test.ts deleted file mode 100644 index 179bc356f..000000000 --- a/tests/regression/tests/issue-1563.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1563', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model ModelA { - id String @id @default(cuid()) - ref ModelB[] - } - - model ModelB { - id String @id @default(cuid()) - ref ModelA? @relation(fields: [refId], references: [id]) - refId String? - - @@validate(refId != null, "refId must be set") - } - `, - { enhancements: ['validation'] } - ); - - const db = enhance(); - - const a = await db.modelA.create({ data: {} }); - const b = await db.modelB.create({ data: { refId: a.id } }); - - await expect(db.modelB.update({ where: { id: b.id }, data: { refId: a.id } })).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1574.test.ts b/tests/regression/tests/issue-1574.test.ts deleted file mode 100644 index 946f54731..000000000 --- a/tests/regression/tests/issue-1574.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1574', () => { - it('regression', async () => { - const { enhance, prisma } = await loadSchema( - ` -model User { - id String @id @default(cuid()) - modelA ModelA[] -} - -// -// ModelA has model-level access-all by owner, but read-all override for the name property -// -model ModelA { - id String @id @default(cuid()) - - owner User @relation(fields: [ownerId], references: [id]) - ownerId String - - name String @allow('read', true, true) - prop2 String? - - refsB ModelB[] - refsC ModelC[] - - @@allow('all', owner == auth()) -} - -// -// ModelB and ModelC are both allow-all everyone. -// They both have a reference to ModelA, but in ModelB that reference is optional. -// -model ModelB { - id String @id @default(cuid()) - - ref ModelA? @relation(fields: [refId], references: [id]) - refId String? - - @@allow('all', true) -} -model ModelC { - id String @id @default(cuid()) - - ref ModelA @relation(fields: [refId], references: [id]) - refId String - - @@allow('all', true) -} - `, - { enhancements: ['policy'] } - ); - - // create two users - const user1 = await prisma.user.create({ data: { id: '1' } }); - const user2 = await prisma.user.create({ data: { id: '2' } }); - - // create two db instances, enhanced for users 1 and 2 - const db1 = enhance(user1); - const db2 = enhance(user2); - - // create a ModelA owned by user1 - const a = await db1.modelA.create({ data: { name: 'a', ownerId: user1.id } }); - - // create a ModelB and a ModelC with refs to ModelA - const b = await db1.modelB.create({ data: { refId: a.id } }); - const c = await db2.modelC.create({ data: { refId: a.id } }); - - // works: user1 should be able to read b as well as the entire referenced a - const t1 = await db1.modelB.findFirst({ select: { ref: true } }); - expect(t1.ref.name).toBeTruthy(); - - // works: user1 also should be able to read b as well as the name of the referenced a - const t2 = await db1.modelB.findFirst({ select: { ref: { select: { name: true } } } }); - expect(t2.ref.name).toBeTruthy(); - - // works: user2 also should be able to read b as well as the name of the referenced a - const t3 = await db2.modelB.findFirst({ select: { ref: { select: { name: true } } } }); - expect(t3.ref.name).toBeTruthy(); - - // works: but user2 should not be able to read b with the entire referenced a - const t4 = await db2.modelB.findFirst({ select: { ref: true } }); - expect(t4.ref).toBeFalsy(); - - // - // The following are essentially the same tests, but with ModelC instead of ModelB - // - - // works: user1 should be able to read c as well as the entire referenced a - const t5 = await db1.modelC.findFirst({ select: { ref: true } }); - expect(t5.ref.name).toBeTruthy(); - - // works: user1 also should be able to read c as well as the name of the referenced a - const t6 = await db1.modelC.findFirst({ select: { ref: { select: { name: true } } } }); - expect(t6.ref.name).toBeTruthy(); - - // works: user2 should not be able to read b along with the a reference. - // In this case, the entire query returns null because of the required (but inaccessible) ref. - await expect(db2.modelC.findFirst({ select: { ref: true } })).toResolveFalsy(); - - // works: if user2 queries c directly and gets the refId to a, it can get the a.name directly - const t7 = await db2.modelC.findFirstOrThrow(); - await expect(db2.modelA.findFirst({ select: { name: true }, where: { id: t7.refId } })).toResolveTruthy(); - - // fails: since the last query worked, we'd expect to be able to query c along with the name of the referenced a directly - await expect(db2.modelC.findFirst({ select: { ref: { select: { name: true } } } })).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1575.test.ts b/tests/regression/tests/issue-1575.test.ts deleted file mode 100644 index 980a75017..000000000 --- a/tests/regression/tests/issue-1575.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1575', () => { - it('regression', async () => { - await loadSchema( - ` - model UserAssets { - id String @id @default(cuid()) - videoId String - videoStream Asset @relation("userVideo", fields: [videoId], references: [id]) - subtitleId String - subtitlesAsset Asset @relation("userSubtitles", fields: [subtitleId], references: [id]) - } - - model Asset { - id String @id @default(cuid()) - type String - userVideo UserAssets[] @relation("userVideo") - userSubtitles UserAssets[] @relation("userSubtitles") - - @@delegate(type) - } - - model Movie extends Asset { - duration Int - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1576.test.ts b/tests/regression/tests/issue-1576.test.ts deleted file mode 100644 index d00b853d1..000000000 --- a/tests/regression/tests/issue-1576.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1576', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` -model Profile { - id Int @id @default(autoincrement()) - name String - items Item[] - type String - @@delegate(type) - @@allow('all', true) -} - -model GoldProfile extends Profile { - ticket Int -} - -model Item { - id Int @id @default(autoincrement()) - profileId Int - profile Profile @relation(fields: [profileId], references: [id]) - type String - @@delegate(type) - @@allow('all', true) -} - -model GoldItem extends Item { - inventory Boolean -} - ` - ); - - const db = enhance(); - - const profile = await db.goldProfile.create({ - data: { - name: 'hello', - ticket: 5, - }, - }); - - await expect( - db.goldItem.createManyAndReturn({ - data: [ - { - profileId: profile.id, - inventory: true, - }, - { - profileId: profile.id, - inventory: true, - }, - ], - }) - ).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), - expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), - ]) - ); - }); -}); diff --git a/tests/regression/tests/issue-1585.test.ts b/tests/regression/tests/issue-1585.test.ts deleted file mode 100644 index 49ec333d4..000000000 --- a/tests/regression/tests/issue-1585.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1585', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Asset { - id Int @id @default(autoincrement()) - type String - views Int - - @@allow('all', true) - @@delegate(type) - } - - model Post extends Asset { - title String - } - ` - ); - - const db = enhance(); - await db.post.create({ data: { title: 'Post1', views: 0 } }); - await db.post.create({ data: { title: 'Post2', views: 1 } }); - await expect( - db.post.count({ - where: { views: { gt: 0 } }, - }) - ).resolves.toBe(1); - }); -}); diff --git a/tests/regression/tests/issue-1596.test.ts b/tests/regression/tests/issue-1596.test.ts deleted file mode 100644 index 62eedfd5f..000000000 --- a/tests/regression/tests/issue-1596.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { isPrismaClientValidationError } from '@zenstackhq/runtime'; -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1596', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - posts Post[] - } - - model Post { - id Int @id - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) - } - ` - ); - - const db = enhance(); - - try { - await db.post.create({ data: { title: 'Post1' } }); - } catch (e) { - // eslint-disable-next-line jest/no-conditional-expect - expect(isPrismaClientValidationError(e)).toBe(true); - return; - } - - throw new Error('Expected error'); - }); -}); diff --git a/tests/regression/tests/issue-1610.test.ts b/tests/regression/tests/issue-1610.test.ts deleted file mode 100644 index a116dbb5d..000000000 --- a/tests/regression/tests/issue-1610.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1610', () => { - it('regular prisma client', async () => { - await loadSchema( - ` - model User { - id Int @id - posts Post[] - } - - model Post { - id Int @id - author User @relation(fields: [authorId], references: [id]) - authorId Int - } - `, - { fullZod: true, output: './lib/zen' } - ); - }); - - it('logical prisma client', async () => { - await loadSchema( - ` - model User { - id Int @id - posts Post[] - } - - model Post { - id Int @id - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) - } - `, - { fullZod: true, output: './lib/zen' } - ); - }); - - it('no custom output', async () => { - await loadSchema( - ` - model User { - id Int @id - posts Post[] - } - - model Post { - id Int @id - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) - } - `, - { fullZod: true, preserveTsFiles: true } - ); - }); -}); diff --git a/tests/regression/tests/issue-1627.test.ts b/tests/regression/tests/issue-1627.test.ts deleted file mode 100644 index cc3c9fe00..000000000 --- a/tests/regression/tests/issue-1627.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1627', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` -model User { - id String @id - memberships GymUser[] -} - -model Gym { - id String @id - members GymUser[] - - @@allow('all', true) -} - -model GymUser { - id String @id - userID String - user User @relation(fields: [userID], references: [id]) - gymID String? - gym Gym? @relation(fields: [gymID], references: [id]) - role String - - @@allow('read',gym.members?[user == auth() && (role == "ADMIN" || role == "TRAINER")]) - @@unique([userID, gymID]) -} - ` - ); - - await prisma.user.create({ data: { id: '1' } }); - - await prisma.gym.create({ - data: { - id: '1', - members: { - create: { - id: '1', - user: { connect: { id: '1' } }, - role: 'ADMIN', - }, - }, - }, - }); - - const db = enhance(); - await expect(db.gymUser.findMany()).resolves.toHaveLength(0); - }); -}); diff --git a/tests/regression/tests/issue-1642.test.ts b/tests/regression/tests/issue-1642.test.ts deleted file mode 100644 index 70c0580e6..000000000 --- a/tests/regression/tests/issue-1642.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1642', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - name String - posts Post[] - - @@allow('read', true) - @@allow('all', auth().id == 1) - } - - model Post { - id Int @id - title String - description String - author User @relation(fields: [authorId], references: [id]) - authorId Int - - // delegate all access policies to the author: - @@allow('all', check(author)) - - @@allow('update', future().title == 'hello') - } - ` - ); - - await prisma.user.create({ data: { id: 1, name: 'User1' } }); - await prisma.post.create({ data: { id: 1, title: 'hello', description: 'desc1', authorId: 1 } }); - - const db = enhance({ id: 2 }); - await expect( - db.post.update({ where: { id: 1 }, data: { title: 'world', description: 'desc2' } }) - ).toBeRejectedByPolicy(); - - await expect(db.post.update({ where: { id: 1 }, data: { description: 'desc2' } })).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1644.test.ts b/tests/regression/tests/issue-1644.test.ts deleted file mode 100644 index 212cf5c99..000000000 --- a/tests/regression/tests/issue-1644.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1644', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - email String @unique @email @length(6, 32) @allow('read', auth() == this) - - // full access to all - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, email: 'a@example.com' } }); - await prisma.user.create({ data: { id: 2, email: 'b@example.com' } }); - - const db = enhance({ id: 1 }); - await expect(db.user.count({ where: { email: { contains: 'example.com' } } })).resolves.toBe(1); - await expect(db.user.findMany({ where: { email: { contains: 'example.com' } } })).resolves.toHaveLength(1); - }); -}); diff --git a/tests/regression/tests/issue-1645.test.ts b/tests/regression/tests/issue-1645.test.ts deleted file mode 100644 index 3bf717f96..000000000 --- a/tests/regression/tests/issue-1645.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1645', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Product { - id String @id @default(cuid()) - name String - slug String - description String? - sku String - price Int - onSale Boolean @default(false) - salePrice Int @default(0) - saleStartDateTime DateTime? - saleEndDateTime DateTime? - scheduledAvailability Boolean @default(false) - availabilityStartDateTime DateTime? - availabilityEndDateTime DateTime? - type String @default('VARIABLE') - image String - orderItems OrderItem[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([slug]) - - @@allow('all', true) - } - - model BaseOrder { - id String @id @default(cuid()) - orderNumber String @unique @default(nanoid(16)) - lineItems OrderItem[] - status String @default('PENDING') - type String @default('PARENT') - userType String? - billingAddress BillingAddress @relation(fields: [billingAddressId], references: [id]) - billingAddressId String @map("billing_address_id") - shippingAddress ShippingAddress @relation(fields: [shippingAddressId], references: [id]) - shippingAddressId String @map("shipping_address_id") - notes String? @default('') - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@allow('all', true) - @@delegate(userType) - } - - model Order extends BaseOrder { - parentId String? @map("parent_id") - parent Order? @relation("OrderToParent", fields: [parentId], references: [id]) - groupedOrders Order[] @relation("OrderToParent") - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String @map("user_id") - } - - model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - orders Order[] - billingAddresses BillingAddress[] - shippingAddresses ShippingAddress[] - - @@allow('create,read', true) - @@allow('update,delete', auth().id == this.id) - } - - model GuestUser { - id String @id @default(cuid()) - name String? - email String @unique - orders GuestOrder[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@auth - @@allow('all', true) - } - - model GuestOrder extends BaseOrder { - guestUser GuestUser @relation(fields: [guestUserId], references: [id], onDelete: Cascade) - guestUserId String @map("guest_user_id") - parentId String? @map("parent_id") - parent GuestOrder? @relation("OrderToParent", fields: [parentId], references: [id]) - groupedOrders GuestOrder[] @relation("OrderToParent") - } - - model OrderItem { - id String @id @default(cuid()) - order BaseOrder @relation(fields: [orderId], references: [id], onDelete: Cascade) - orderId String @map("order_id") - product Product @relation(fields: [productId], references: [id]) - productId String @map("product_id") - quantity Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@allow('all', true) - } - - model OrderAddress { - id String @id @default(cuid()) - firstName String - lastName String - address1 String - address2 String? - city String - state String - postalCode String - country String - email String - phone String - type String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@allow('all', true) - @@delegate(type) - } - - model BillingAddress extends OrderAddress { - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String? @map("user_id") - order BaseOrder[] - } - - model ShippingAddress extends OrderAddress { - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String? @map("user_id") - order BaseOrder[] - } - ` - ); - - const db = enhance(); - - await db.user.create({ data: { id: '1', name: 'John', email: 'john@example.com' } }); - - const shipping = await db.shippingAddress.create({ - data: { - id: '1', - firstName: 'John', - lastName: 'Doe', - address1: '123 Main St', - city: 'Anytown', - state: 'CA', - postalCode: '12345', - country: 'US', - email: 'john@example.com', - phone: '123-456-7890', - user: { connect: { id: '1' } }, - }, - }); - - const billing = await db.billingAddress.create({ - data: { - id: '2', - firstName: 'John', - lastName: 'Doe', - address1: '123 Main St', - city: 'Anytown', - state: 'CA', - postalCode: '12345', - country: 'US', - email: 'john@example.com', - phone: '123-456-7890', - user: { connect: { id: '1' } }, - }, - }); - - await db.order.create({ - data: { - id: '1', - orderNumber: '1', - status: 'PENDING', - type: 'PARENT', - shippingAddress: { connect: { id: '1' } }, - billingAddress: { connect: { id: '2' } }, - user: { connect: { id: '1' } }, - }, - }); - - const updated = await db.order.update({ - where: { id: '1' }, - include: { - lineItems: true, - billingAddress: true, - shippingAddress: true, - }, - data: { - type: 'CAMPAIGN', - }, - }); - - console.log(updated); - expect(updated.shippingAddress).toEqual(shipping); - expect(updated.billingAddress).toEqual(billing); - }); -}); diff --git a/tests/regression/tests/issue-1647.test.ts b/tests/regression/tests/issue-1647.test.ts deleted file mode 100644 index e93f63cfb..000000000 --- a/tests/regression/tests/issue-1647.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import fs from 'fs'; - -describe('issue 1647', () => { - it('inherits @@schema by default', async () => { - const { projectDir } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - schemas = ['public', 'post'] - } - - generator client { - provider = 'prisma-client-js' - previewFeatures = ['multiSchema'] - } - - model Asset { - id Int @id - type String - @@delegate(type) - @@schema('public') - } - - model Post extends Asset { - title String - } - `, - { addPrelude: false, pushDb: false, getPrismaOnly: true } - ); - - const prismaSchema = fs.readFileSync(`${projectDir}/prisma/schema.prisma`, 'utf-8'); - expect(prismaSchema.split('\n').filter((l) => l.includes('@@schema("public")'))).toHaveLength(2); - }); - it('respects sub model @@schema overrides', async () => { - const { projectDir } = await loadSchema( - ` - datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') - schemas = ['public', 'post'] - } - - generator client { - provider = 'prisma-client-js' - previewFeatures = ['multiSchema'] - } - - model Asset { - id Int @id - type String - @@delegate(type) - @@schema('public') - } - - model Post extends Asset { - title String - @@schema('post') - } - `, - { addPrelude: false, pushDb: false, getPrismaOnly: true } - ); - - const prismaSchema = fs.readFileSync(`${projectDir}/prisma/schema.prisma`, 'utf-8'); - expect(prismaSchema.split('\n').filter((l) => l.includes('@@schema("public")'))).toHaveLength(1); - expect(prismaSchema.split('\n').filter((l) => l.includes('@@schema("post")'))).toHaveLength(1); - }); -}); diff --git a/tests/regression/tests/issue-1648.test.ts b/tests/regression/tests/issue-1648.test.ts deleted file mode 100644 index 67a19e0ed..000000000 --- a/tests/regression/tests/issue-1648.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1648', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - posts Post[] - } - - model Profile { - id Int @id @default(autoincrement()) - someText String - user User @relation(fields: [userId], references: [id]) - userId Int @unique - } - - model Post { - id Int @id @default(autoincrement()) - title String - - userId Int - user User @relation(fields: [userId], references: [id]) - - // this will always be true, even if the someText field is "canUpdate" - @@deny("update", future().user.profile.someText != "canUpdate") - - @@allow("all", true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, profile: { create: { someText: 'canUpdate' } } } }); - await prisma.user.create({ data: { id: 2, profile: { create: { someText: 'nothing' } } } }); - await prisma.post.create({ data: { id: 1, title: 'Post1', userId: 1 } }); - await prisma.post.create({ data: { id: 2, title: 'Post2', userId: 2 } }); - - const db = enhance(); - await expect(db.post.update({ where: { id: 1 }, data: { title: 'Post1-1' } })).toResolveTruthy(); - await expect(db.post.update({ where: { id: 2 }, data: { title: 'Post2-2' } })).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/regression/tests/issue-1667.test.ts b/tests/regression/tests/issue-1667.test.ts deleted file mode 100644 index 0e84d5f14..000000000 --- a/tests/regression/tests/issue-1667.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1667', () => { - it('custom enhance standard zod output', async () => { - await loadSchema( - ` - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - - plugin enhancer { - provider = '@core/enhancer' - output = './zen' - } - - model User { - id Int @id - email String @unique @email - } - `, - { addPrelude: false, getPrismaOnly: true, preserveTsFiles: true } - ); - }); - - it('custom enhance custom zod output', async () => { - await loadSchema( - ` - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - - plugin enhancer { - provider = '@core/enhancer' - output = './zen' - } - - plugin zod { - provider = '@core/zod' - output = './myzod' - } - - model User { - id Int @id - email String @unique @email - } - `, - { addPrelude: false, getPrismaOnly: true, generateNoCompile: true, compile: true } - ); - }); - - it('standard enhance custom zod output', async () => { - await loadSchema( - ` - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - - plugin zod { - provider = '@core/zod' - output = './myzod' - } - - model User { - id Int @id - email String @unique @email - } - `, - { addPrelude: false, getPrismaOnly: true, compile: true } - ); - }); -}); diff --git a/tests/regression/tests/issue-1674.test.ts b/tests/regression/tests/issue-1674.test.ts deleted file mode 100644 index 0928e5464..000000000 --- a/tests/regression/tests/issue-1674.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1674', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - email String @unique @email @length(6, 32) - password String @password @omit - posts Post[] - - // everybody can signup - @@allow('create', true) - - // full access by self - @@allow('all', auth() == this) - } - - model Blog { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - post Post? @relation(fields: [postId], references: [id], onDelete: Cascade) - postId String? - } - - model Post { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - title String @length(1, 256) - content String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String - - blogs Blog[] - - type String - - // allow read for all signin users - @@allow('read', auth() != null && published) - - // full access by author - @@allow('all', author == auth()) - - @@delegate(type) - } - - model PostA extends Post { - } - - model PostB extends Post { - } - ` - ); - - const user = await prisma.user.create({ - data: { email: 'abc@def.com', password: 'password' }, - }); - - const blog = await prisma.blog.create({ - data: {}, - }); - - const db = enhance(user); - await expect( - db.postA.create({ - data: { - content: 'content', - title: 'title', - blogs: { - connect: { - id: blog.id, - }, - }, - author: { - connect: { - id: user.id, - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/regression/tests/issue-1681.test.ts b/tests/regression/tests/issue-1681.test.ts deleted file mode 100644 index e3b7bd630..000000000 --- a/tests/regression/tests/issue-1681.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1681', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - posts Post[] - @@allow('all', true) - } - - model Post { - id Int @id @default(autoincrement()) - title String - author User @relation(fields: [authorId], references: [id]) - authorId Int @default(auth().id) - @@allow('all', true) - } - ` - ); - - const db = enhance({ id: 1 }); - const user = await db.user.create({ data: {} }); - await expect(db.post.createMany({ data: [{ title: 'Post1' }] })).resolves.toMatchObject({ count: 1 }); - - const r = await db.post.createManyAndReturn({ data: [{ title: 'Post2' }] }); - expect(r[0].authorId).toBe(user.id); - }); -}); diff --git a/tests/regression/tests/issue-1693.test.ts b/tests/regression/tests/issue-1693.test.ts deleted file mode 100644 index cf61b286c..000000000 --- a/tests/regression/tests/issue-1693.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1693', () => { - it('regression', async () => { - await loadSchema( - ` - model Animal { - id String @id @default(uuid()) - animalType String @default("") - @@delegate(animalType) - } - - model Dog extends Animal { - name String - } - `, - { fullZod: true } - ); - }); -}); diff --git a/tests/regression/tests/issue-1695.test.ts b/tests/regression/tests/issue-1695.test.ts deleted file mode 100644 index c58417265..000000000 --- a/tests/regression/tests/issue-1695.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { loadModel } from '@zenstackhq/testtools'; - -describe('issue 1695', () => { - it('regression', async () => { - await loadModel( - ` - abstract model SoftDelete { - deleted Int @default(0) @omit - } - - model MyModel extends SoftDelete { - id String @id @default(cuid()) - name String - - @@deny('update', deleted != 0 && future().deleted != 0) - @@deny('read', this.deleted != 0) - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1698.test.ts b/tests/regression/tests/issue-1698.test.ts deleted file mode 100644 index 0d9750785..000000000 --- a/tests/regression/tests/issue-1698.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1698', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model House { - id Int @id @default(autoincrement()) - doorTypeId Int - door Door @relation(fields: [doorTypeId], references: [id]) - houseType String - @@delegate(houseType) - } - - model PrivateHouse extends House { - size Int - } - - model Skyscraper extends House { - height Int - } - - model Door { - id Int @id @default(autoincrement()) - color String - doorType String - houses House[] - @@delegate(doorType) - } - - model IronDoor extends Door { - strength Int - } - - model WoodenDoor extends Door { - texture String - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const door1 = await db.ironDoor.create({ - data: { strength: 100, color: 'blue' }, - }); - console.log(door1); - - const door2 = await db.woodenDoor.create({ - data: { texture: 'pine', color: 'red' }, - }); - console.log(door2); - - const house1 = await db.privateHouse.create({ - data: { size: 5000, door: { connect: { id: door1.id } } }, - }); - console.log(house1); - - const house2 = await db.skyscraper.create({ - data: { height: 3000, door: { connect: { id: door2.id } } }, - }); - console.log(house2); - - const r1 = await db.privateHouse.findFirst({ include: { door: true } }); - console.log(r1); - expect(r1).toMatchObject({ - door: { color: 'blue', strength: 100 }, - }); - - const r2 = (await db.skyscraper.findMany({ include: { door: true } }))[0]; - console.log(r2); - expect(r2).toMatchObject({ - door: { color: 'red', texture: 'pine' }, - }); - }); -}); diff --git a/tests/regression/tests/issue-1710.test.ts b/tests/regression/tests/issue-1710.test.ts deleted file mode 100644 index 796c403b4..000000000 --- a/tests/regression/tests/issue-1710.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1710', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Profile { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - displayName String - type String - - @@delegate(type) - @@allow('read,create', true) - } - - model User extends Profile { - email String @unique @deny('read', true) - password String @omit - role String @default('USER') @deny('read,update', true) - } - - model Organization extends Profile {} - ` - ); - - const db = enhance(); - const user = await db.user.create({ - data: { displayName: 'User1', email: 'a@b.com', password: '123' }, - }); - expect(user.email).toBeUndefined(); - expect(user.password).toBeUndefined(); - - const foundUser = await db.profile.findUnique({ where: { id: user.id } }); - expect(foundUser.email).toBeUndefined(); - expect(foundUser.password).toBeUndefined(); - - await expect( - db.profile.update({ - where: { - id: user.id, - }, - data: { - delegate_aux_user: { - update: { - role: 'ADMIN', - }, - }, - }, - }) - ).rejects.toThrow('Auxiliary relation field'); - }); -}); diff --git a/tests/regression/tests/issue-1734.test.ts b/tests/regression/tests/issue-1734.test.ts deleted file mode 100644 index 6cfaff397..000000000 --- a/tests/regression/tests/issue-1734.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1734', () => { - it('regression', async () => { - const { enhance, enhanceRaw, prisma } = await loadSchema( - ` - abstract model Base { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - } - - model Profile extends Base { - displayName String - type String - - @@allow('read', true) - @@delegate(type) - } - - model User extends Profile { - username String @unique - access Access[] - organization Organization[] - } - - model Access extends Base { - user User @relation(fields: [userId], references: [id]) - userId String - - organization Organization @relation(fields: [organizationId], references: [id]) - organizationId String - - manage Boolean @default(false) - - superadmin Boolean @default(false) - - @@unique([userId,organizationId]) - } - - model Organization extends Profile { - owner User @relation(fields: [ownerId], references: [id]) - ownerId String @default(auth().id) - published Boolean @default(false) @allow('read', access?[user == auth()]) - access Access[] - } - - ` - ); - const db = enhance(); - const rootDb = enhanceRaw(prisma, undefined, { - kinds: ['delegate'], - }); - - const user = await rootDb.user.create({ - data: { - username: 'test', - displayName: 'test', - }, - }); - - const organization = await rootDb.organization.create({ - data: { - displayName: 'test', - owner: { - connect: { - id: user.id, - }, - }, - access: { - create: { - user: { - connect: { - id: user.id, - }, - }, - manage: true, - superadmin: true, - }, - }, - }, - }); - - const foundUser = await db.profile.findFirst({ - where: { - id: user.id, - }, - }); - expect(foundUser).toMatchObject(user); - - const foundOrg = await db.profile.findFirst({ - where: { - id: organization.id, - }, - }); - // published field not readable - expect(foundOrg).toMatchObject({ id: organization.id, displayName: 'test', type: 'Organization' }); - expect(foundOrg.published).toBeUndefined(); - - const foundOrg1 = await enhance({ id: user.id }).profile.findFirst({ - where: { - id: organization.id, - }, - }); - // published field readable - expect(foundOrg1.published).not.toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-1743.test.ts b/tests/regression/tests/issue-1743.test.ts deleted file mode 100644 index 1c379ae44..000000000 --- a/tests/regression/tests/issue-1743.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1743', () => { - it('regression', async () => { - await loadSchema( - ` - generator client { - provider = "prisma-client-js" - output = '../lib/zenstack/prisma' - } - - datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - - plugin enhancer { - provider = '@core/enhancer' - output = './lib/zenstack' - compile = false - } - - model User { - id Int @id - } - `, - { - addPrelude: false, - compile: true, - output: './lib/zenstack', - prismaLoadPath: './lib/zenstack/prisma', - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1745.test.ts b/tests/regression/tests/issue-1745.test.ts deleted file mode 100644 index ece70d6ee..000000000 --- a/tests/regression/tests/issue-1745.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1745', () => { - it('regression', async () => { - const dbUrl = await createPostgresDb('issue-1745'); - - try { - await loadSchema( - ` - enum BuyerType { - STORE - RESTAURANT - WHOLESALER - } - - enum ChainStore { - ALL - CHAINSTORE_1 - CHAINSTORE_2 - CHAINSTORE_3 - } - - abstract model Id { - id String @id @default(cuid()) - } - - abstract model Base extends Id { - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - } - - model Ad extends Base { - serial Int @unique @default(autoincrement()) - buyerTypes BuyerType[] - chainStores ChainStore[] - listPrice Float - isSold Boolean @default(false) - - supplier Supplier @relation(fields: [supplierId], references: [id]) - supplierId String @default(auth().companyId) - - @@allow('all', auth().company.companyType == 'Buyer' && has(buyerTypes, auth().company.buyerType)) - @@allow('all', auth().company.companyType == 'Supplier' && auth().companyId == supplierId) - @@allow('all', auth().isAdmin) - } - - model Company extends Base { - name String @unique - organizationNumber String @unique - users User[] - buyerType BuyerType - - companyType String - @@delegate(companyType) - - @@allow('read, update', auth().companyId == id) - @@allow('all', auth().isAdmin) - } - - model Buyer extends Company { - storeName String - type String - chainStore ChainStore @default(ALL) - - @@allow('read, update', auth().company.companyType == 'Buyer' && auth().companyId == id) - @@allow('all', auth().isAdmin) - } - - model Supplier extends Company { - ads Ad[] - - @@allow('all', auth().company.companyType == 'Supplier' && auth().companyId == id) - @@allow('all', auth().isAdmin) - } - - model User extends Base { - firstName String - lastName String - email String @unique - username String @unique - password String @password @omit - isAdmin Boolean @default(false) - - company Company? @relation(fields: [companyId], references: [id]) - companyId String? - - @@allow('read', auth().id == id) - @@allow('read', auth().companyId == companyId) - @@allow('all', auth().isAdmin) - } - `, - { provider: 'postgresql', dbUrl, pushDb: false } - ); - } finally { - dropPostgresDb('issue-1745'); - } - }); -}); diff --git a/tests/regression/tests/issue-1746.test.ts b/tests/regression/tests/issue-1746.test.ts deleted file mode 100644 index ac74a0124..000000000 --- a/tests/regression/tests/issue-1746.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -describe('issue 1746', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - - plugin zod { - provider = '@core/zod' - mode = 'strip' - preserveTsFiles = true - } - - model Submission { - id String @id @default(uuid()) - title String? - userId String? - user User? @relation(fields: [userId], references: [id], name: "user") - comments Comment[] @relation("submission") - @@allow("all", true) - } - - model User { - id String @id @default(uuid()) - name String? - submissions Submission[] @relation("user") - comments Comment[] @relation("user") - @@allow("all", true) - } - - model Comment { - id String @id @default(uuid()) - content String? - userId String - user User @relation(fields: [userId], references: [id], name: "user") - submissionId String - submission Submission @relation(fields: [submissionId], references: [id], name: "submission") - @@allow("all", true) - } - `, - { addPrelude: false } - ); - - const commentCreateInputSchema = zodSchemas.input.CommentInputSchema.create; - - // unchecked - let parsed = commentCreateInputSchema.safeParse({ - data: { - content: 'Comment', - userId: '1', - submissionId: '2', - unknown: 'unknown', - }, - }); - expect(parsed.success).toBe(true); - expect(parsed.data.data.userId).toBe('1'); - expect(parsed.data.data.submissionId).toBe('2'); - expect(parsed.data.data.unknown).toBeUndefined(); - - // checked - parsed = commentCreateInputSchema.safeParse({ - data: { - content: 'Comment', - user: { connect: { id: '1' } }, - submission: { connect: { id: '2' } }, - unknown: 'unknown', - }, - }); - expect(parsed.success).toBe(true); - expect(parsed.data.data.user).toMatchObject({ connect: { id: '1' } }); - expect(parsed.data.data.submission).toMatchObject({ connect: { id: '2' } }); - expect(parsed.data.data.unknown).toBeUndefined(); - - // mixed - parsed = commentCreateInputSchema.safeParse({ - data: { - content: 'Comment', - userId: '1', - submission: { connect: { id: '2' } }, - unknown: 'unknown', - }, - }); - expect(parsed.success).toBe(false); - - // nested create schema: checked/unchecked/array union - const commentCreateNestedMany = zodSchemas.objects.CommentCreateNestedManyWithoutSubmissionInputObjectSchema; - - // unchecked - parsed = commentCreateNestedMany.safeParse({ - create: { userId: '1', content: 'Content', unknown: 'unknown' }, - }); - expect(parsed.success).toBe(true); - expect(parsed.data.create.userId).toBe('1'); - expect(parsed.data.create.unknown).toBeUndefined(); - - // empty array - parsed = commentCreateNestedMany.safeParse({ create: [] }); - expect(parsed.success).toBe(true); - expect(parsed.data.create).toHaveLength(0); - - // unchecked array - parsed = commentCreateNestedMany.safeParse({ - create: [ - { userId: '1', content: 'Content1', unknown: 'unknown1' }, - { userId: '2', content: 'Content2', unknown: 'unknown2' }, - ], - }); - expect(parsed.success).toBe(true); - expect(parsed.data.create).toHaveLength(2); - expect(parsed.data.create[0].userId).toBe('1'); - expect(parsed.data.create[0].unknown).toBeUndefined(); - - // checked - parsed = commentCreateNestedMany.safeParse({ - create: { user: { connect: { id: '1' } }, content: 'Content', unknown: 'unknown' }, - }); - expect(parsed.success).toBe(true); - expect(parsed.data.create.user).toMatchObject({ connect: { id: '1' } }); - expect(parsed.data.create.unknown).toBeUndefined(); - - // checked array - parsed = commentCreateNestedMany.safeParse({ - create: [ - { user: { connect: { id: '1' } }, content: 'Content1', unknown: 'unknown1' }, - { user: { connect: { id: '2' } }, content: 'Content2', unknown: 'unknown2' }, - ], - }); - expect(parsed.success).toBe(true); - expect(parsed.data.create).toHaveLength(2); - expect(parsed.data.create[0].user).toMatchObject({ connect: { id: '1' } }); - expect(parsed.data.create[0].unknown).toBeUndefined(); - - // mixed - parsed = commentCreateNestedMany.safeParse({ - create: [ - { user: { connect: { id: '1' } }, content: 'Content1', unknown: 'unknown1' }, - { userId: '1', content: 'Content2', unknown: 'unknown2' }, - ], - }); - expect(parsed.success).toBe(false); - }); -}); diff --git a/tests/regression/tests/issue-1755.test.ts b/tests/regression/tests/issue-1755.test.ts deleted file mode 100644 index 9e41f7c6e..000000000 --- a/tests/regression/tests/issue-1755.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1755', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - contents Content[] - } - - model Content { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) - userId Int - contentType String - @@delegate(contentType) - } - - model Post extends Content { - title String - } - - model Video extends Content { - name String - duration Int - } - `, - { enhancements: ['delegate'] } - ); - - const db = enhance(); - const user = await db.user.create({ data: {} }); - const now = Date.now(); - await db.post.create({ - data: { title: 'post1', createdAt: new Date(now - 1000), user: { connect: { id: user.id } } }, - }); - await db.post.create({ - data: { title: 'post2', createdAt: new Date(now), user: { connect: { id: user.id } } }, - }); - - // scalar orderBy - await expect(db.post.findFirst({ orderBy: { createdAt: 'desc' } })).resolves.toMatchObject({ - title: 'post2', - }); - - // array orderBy - await expect(db.post.findFirst({ orderBy: [{ createdAt: 'desc' }] })).resolves.toMatchObject({ - title: 'post2', - }); - - // nested orderBy - await expect( - db.user.findFirst({ include: { contents: { orderBy: [{ createdAt: 'desc' }] } } }) - ).resolves.toMatchObject({ - id: user.id, - contents: [{ title: 'post2' }, { title: 'post1' }], - }); - }); -}); diff --git a/tests/regression/tests/issue-1758.test.ts b/tests/regression/tests/issue-1758.test.ts deleted file mode 100644 index f07428e8e..000000000 --- a/tests/regression/tests/issue-1758.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { loadModelWithError } from '@zenstackhq/testtools'; - -describe('issue 1758', () => { - it('regression', async () => { - await expect( - loadModelWithError( - ` - model Organization { - id String @id @default(cuid()) - contents Content[] @relation("OrganizationContents") - } - - model Content { - id String @id @default(cuid()) - contentType String - organization Organization @relation("OrganizationContents", fields: [organizationId], references: [id]) - organizationId String - @@delegate(contentType) - } - - model Store extends Content { - name String - @@unique([organizationId, name]) - } - ` - ) - ).resolves.toContain('Cannot use fields inherited from a polymorphic base model in `@@unique`'); - }); -}); diff --git a/tests/regression/tests/issue-1763.test.ts b/tests/regression/tests/issue-1763.test.ts deleted file mode 100644 index d5ea1d401..000000000 --- a/tests/regression/tests/issue-1763.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1763', () => { - it('regression', async () => { - await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - name String - - type String - @@delegate(type) - - // full access by author - @@allow('all', true) - } - - model ConcretePost extends Post { - age Int - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient as Prisma } from '@prisma/client'; -import { enhance } from '@zenstackhq/runtime'; - -async function test() { - const prisma = new Prisma(); - const db = enhance(prisma); - await db.concretePost.create({ - data: { - id: 5, - name: 'a name', - age: 20, - }, - }); -} `, - }, - ], - } - ); - }); -}); diff --git a/tests/regression/tests/issue-177.test.ts b/tests/regression/tests/issue-177.test.ts deleted file mode 100644 index d270580c5..000000000 --- a/tests/regression/tests/issue-177.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { loadModelWithError } from '@zenstackhq/testtools'; - -describe('issue 177', () => { - it('regression', async () => { - await expect( - loadModelWithError( - ` - model Foo { - id String @id @default(cuid()) - - bar Bar @relation(fields: [barId1, barId2], references: [id1, id2]) - barId1 String? - barId2 String - } - - model Bar { - id1 String @default(cuid()) - id2 String @default(cuid()) - foos Foo[] - - @@id([id1, id2]) - } - ` - ) - ).resolves.toContain('relation "bar" is not optional, but field "barId1" is optional'); - }); -}); diff --git a/tests/regression/tests/issue-1770.test.ts b/tests/regression/tests/issue-1770.test.ts deleted file mode 100644 index 03af5cd19..000000000 --- a/tests/regression/tests/issue-1770.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1770', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - orgs OrgUser[] - } - - model OrgUser { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) - userId String - org Organization @relation(fields: [orgId], references: [id]) - orgId String - } - - model Organization { - id String @id @default(uuid()) - users OrgUser[] - resources Resource[] @relation("organization") - } - - abstract model BaseAuth { - id String @id @default(uuid()) - organizationId String? - organization Organization? @relation(fields: [organizationId], references: [id], name: "organization") - - @@allow('all', organization.users?[user == auth()]) - } - - model Resource extends BaseAuth { - name String? - type String? - - @@delegate(type) - } - - model Personnel extends Resource { - } - ` - ); - - const db = enhance(); - await expect(db.resource.findMany()).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1786.test.ts b/tests/regression/tests/issue-1786.test.ts deleted file mode 100644 index ae37297de..000000000 --- a/tests/regression/tests/issue-1786.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1786', () => { - it('regression', async () => { - await loadSchema( - ` - model User { - id String @id @default(cuid()) - email String @unique @email @length(6, 32) - password String @password @omit - contents Content[] - - // everybody can signup - @@allow('create', true) - - // full access by self - @@allow('all', auth() == this) - } - - abstract model BaseContent { - published Boolean @default(false) - - @@index([published]) - } - - model Content extends BaseContent { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id]) - ownerId String - contentType String - - @@delegate(contentType) - } - - model Post extends Content { - title String - } - - model Video extends Content { - name String - duration Int - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1835.test.ts b/tests/regression/tests/issue-1835.test.ts deleted file mode 100644 index f29efcbbb..000000000 --- a/tests/regression/tests/issue-1835.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1835', () => { - it('regression', async () => { - await loadSchema( - ` - enum Enum { - SOME_VALUE - ANOTHER_VALUE - } - - model Model { - id String @id @default(cuid()) - value Enum - @@ignore - } - - model AnotherModel { - id String @id @default(cuid()) - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1843.test.ts b/tests/regression/tests/issue-1843.test.ts deleted file mode 100644 index 518262857..000000000 --- a/tests/regression/tests/issue-1843.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1843', () => { - it('regression', async () => { - const { zodSchemas, enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - email String @unique @email @length(6, 32) - password String @password @omit - contents Content[] - postsCoauthored PostWithCoauthor[] - - @@allow('all', true) - } - - abstract model Owner { - owner User @relation(fields: [ownerId], references: [id]) - ownerId String @default(auth().id) - } - - abstract model BaseContent extends Owner { - published Boolean @default(false) - - @@index([published]) - } - - model Content extends BaseContent { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - contentType String - @@allow('all', true) - - @@delegate(contentType) - } - - model PostWithCoauthor extends Content { - title String - - coauthor User @relation(fields: [coauthorId], references: [id]) - coauthorId String - - @@allow('all', true) - } - - model Post extends Content { - title String - - @@allow('all', true) - } - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - - async function main() { - const enhanced = enhance(new PrismaClient()); - await enhanced.postWithCoauthor.create({ - data: { - title: "new post", - coauthor: { - connect: { - id: "1" - } - }, - } - }); - - await enhanced.postWithCoauthor.create({ - data: { - title: "new post", - coauthorId: "1" - } - }); - } - `, - }, - ], - } - ); - - const user = await prisma.user.create({ data: { email: 'abc', password: '123' } }); - const db = enhance({ id: user.id }); - - // connect - await expect( - db.postWithCoauthor.create({ data: { title: 'new post', coauthor: { connect: { id: user.id } } } }) - ).toResolveTruthy(); - - // fk setting - await expect( - db.postWithCoauthor.create({ data: { title: 'new post', coauthorId: user.id } }) - ).toResolveTruthy(); - - // zod validation - zodSchemas.models.PostWithCoauthorCreateSchema.parse({ - title: 'new post', - coauthorId: '1', - }); - }); -}); diff --git a/tests/regression/tests/issue-1849.test.ts b/tests/regression/tests/issue-1849.test.ts deleted file mode 100644 index 2362ac668..000000000 --- a/tests/regression/tests/issue-1849.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { FILE_SPLITTER, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1849', () => { - it('regression', async () => { - await loadSchema( - `schema.zmodel - import './enum' - - model Post { - id Int @id - status Status @default(PUBLISHED) - } - - ${FILE_SPLITTER}enum.zmodel - - enum Status { - PENDING - PUBLISHED - } - `, - { provider: 'postgresql', pushDb: false } - ); - }); -}); diff --git a/tests/regression/tests/issue-1857.test.ts b/tests/regression/tests/issue-1857.test.ts deleted file mode 100644 index fcd730017..000000000 --- a/tests/regression/tests/issue-1857.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1857', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` - type JSONContent { - type String - text String? - } - - model Post { - id String @id @default(uuid()) - content JSONContent @json - @@allow('all', true) - } - `, - { - provider: 'postgresql', - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - - async function main() { - const prisma = new PrismaClient(); - await prisma.post.create({ - data: { - content: { type: 'foo', text: null } - } - }); - } - `, - }, - ], - } - ); - - zodSchemas.models.JSONContentSchema.parse({ type: 'foo', text: null }); - }); -}); diff --git a/tests/regression/tests/issue-1859.test.ts b/tests/regression/tests/issue-1859.test.ts deleted file mode 100644 index 2b9d4538b..000000000 --- a/tests/regression/tests/issue-1859.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1859', () => { - it('extend enhanced client', async () => { - const { enhance, prisma } = await loadSchema( - ` - model Post { - id Int @id - title String - published Boolean - - @@allow('create', true) - @@allow('read', published) - } - ` - ); - - await prisma.post.create({ data: { id: 1, title: 'post1', published: true } }); - await prisma.post.create({ data: { id: 2, title: 'post2', published: false } }); - - const db = enhance(); - await expect(db.post.findMany()).resolves.toHaveLength(1); - - const extended = db.$extends({ - model: { - post: { - findManyListView: async (args: any) => { - return { view: true, data: await db.post.findMany(args) }; - }, - }, - }, - }); - - await expect(extended.post.findManyListView()).resolves.toMatchObject({ - view: true, - data: [{ id: 1, title: 'post1', published: true }], - }); - await expect(extended.post.findMany()).resolves.toHaveLength(1); - }); - - it('enhance extended client', async () => { - const { enhanceRaw, prisma, prismaModule } = await loadSchema( - ` - model Post { - id Int @id - title String - published Boolean - - @@allow('create', true) - @@allow('read', published) - } - ` - ); - - await prisma.post.create({ data: { id: 1, title: 'post1', published: true } }); - await prisma.post.create({ data: { id: 2, title: 'post2', published: false } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - model: { - post: { - findManyListView: async (args: any) => { - return { view: true, data: await prisma.post.findMany(args) }; - }, - }, - }, - }); - }); - - await expect(prisma.$extends(ext).post.findMany()).resolves.toHaveLength(2); - await expect(prisma.$extends(ext).post.findManyListView()).resolves.toMatchObject({ - view: true, - data: [ - { id: 1, title: 'post1', published: true }, - { id: 2, title: 'post2', published: false }, - ], - }); - - const enhanced = enhanceRaw(prisma.$extends(ext)); - await expect(enhanced.post.findMany()).resolves.toHaveLength(1); - // findManyListView internally uses the un-enhanced client - await expect(enhanced.post.findManyListView()).resolves.toMatchObject({ - view: true, - data: [ - { id: 1, title: 'post1', published: true }, - { id: 2, title: 'post2', published: false }, - ], - }); - }); -}); diff --git a/tests/regression/tests/issue-1870.test.ts b/tests/regression/tests/issue-1870.test.ts deleted file mode 100644 index aa392e54a..000000000 --- a/tests/regression/tests/issue-1870.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { loadModel, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1870', () => { - it('regression', async () => { - await loadModel( - ` - model Polygon { - id Int @id @default(autoincrement()) - geometry Unsupported("geometry(MultiPolygon, 4326)") - @@index([geometry], name: "parcel_polygon_idx", type: Gist) - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-1894.test.ts b/tests/regression/tests/issue-1894.test.ts deleted file mode 100644 index e0ca79030..000000000 --- a/tests/regression/tests/issue-1894.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1894', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model A { - id Int @id @default(autoincrement()) - b B[] - } - - model B { - id Int @id @default(autoincrement()) - a A @relation(fields: [aId], references: [id]) - aId Int - - type String - @@delegate(type) - } - - model C extends B { - f String? - } - `, - { - enhancements: ['delegate'], - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { enhance } from '.zenstack/enhance'; - import { PrismaClient } from '@prisma/client'; - - async function main() { - const db = enhance(new PrismaClient()); - await db.a.create({ data: { id: 0 } }); - await db.c.create({ data: { a: { connect: { id: 0 } } } }); - } - - main(); - - `, - }, - ], - } - ); - - const db = enhance(); - await db.a.create({ data: { id: 0 } }); - await expect(db.c.create({ data: { a: { connect: { id: 0 } } } })).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1930.test.ts b/tests/regression/tests/issue-1930.test.ts deleted file mode 100644 index 762369321..000000000 --- a/tests/regression/tests/issue-1930.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1930', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` -model Organization { - id String @id @default(cuid()) - entities Entity[] - - @@allow('all', true) -} - -model Entity { - id String @id @default(cuid()) - org Organization? @relation(fields: [orgId], references: [id]) - orgId String? - contents EntityContent[] - entityType String - isDeleted Boolean @default(false) - - @@delegate(entityType) - - @@allow('all', !isDeleted) -} - -model EntityContent { - id String @id @default(cuid()) - entity Entity @relation(fields: [entityId], references: [id]) - entityId String - - entityContentType String - - @@delegate(entityContentType) - - @@allow('create', true) - @@allow('read', check(entity)) -} - -model Article extends Entity { -} - -model ArticleContent extends EntityContent { - body String? -} - -model OtherContent extends EntityContent { - data Int -} - ` - ); - - const fullDb = enhance(undefined, { kinds: ['delegate'] }); - const org = await fullDb.organization.create({ data: {} }); - const article = await fullDb.article.create({ - data: { org: { connect: { id: org.id } } }, - }); - - const db = enhance(); - - // normal create/read - await expect( - db.articleContent.create({ - data: { body: 'abc', entity: { connect: { id: article.id } } }, - }) - ).toResolveTruthy(); - await expect(db.article.findFirst({ include: { contents: true } })).resolves.toMatchObject({ - contents: expect.arrayContaining([expect.objectContaining({ body: 'abc' })]), - }); - - // deleted article's contents are not readable - const deletedArticle = await fullDb.article.create({ - data: { org: { connect: { id: org.id } }, isDeleted: true }, - }); - const content1 = await fullDb.articleContent.create({ - data: { body: 'bcd', entity: { connect: { id: deletedArticle.id } } }, - }); - await expect(db.articleContent.findUnique({ where: { id: content1.id } })).toResolveNull(); - }); -}); diff --git a/tests/regression/tests/issue-1955.test.ts b/tests/regression/tests/issue-1955.test.ts deleted file mode 100644 index 703dd0f44..000000000 --- a/tests/regression/tests/issue-1955.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1955', () => { - it('simple policy', async () => { - const dbUrl = await createPostgresDb('issue-1955-1'); - let _prisma: any; - - try { - const { enhance, prisma } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - name String - expections String[] - - @@allow('all', true) - } - `, - { provider: 'postgresql', dbUrl } - ); - _prisma = prisma; - - const db = enhance(); - - await expect( - db.post.createManyAndReturn({ - data: [ - { - name: 'bla', - }, - { - name: 'blu', - }, - ], - }) - ).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'bla' }), - expect.objectContaining({ name: 'blu' }), - ]) - ); - - await expect( - db.post.updateManyAndReturn({ - data: { name: 'foo' }, - }) - ).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'foo' }), - expect.objectContaining({ name: 'foo' }), - ]) - ); - } finally { - await _prisma.$disconnect(); - await dropPostgresDb('issue-1955-1'); - } - }); - - it('complex policy', async () => { - const dbUrl = await createPostgresDb('issue-1955-2'); - let _prisma: any; - - try { - const { enhance, prisma } = await loadSchema( - ` - model Post { - id Int @id @default(autoincrement()) - name String - expections String[] - comments Comment[] - - @@allow('all', comments^[private]) - } - - model Comment { - id Int @id @default(autoincrement()) - private Boolean @default(false) - postId Int - post Post @relation(fields: [postId], references: [id]) - } - `, - { provider: 'postgresql', dbUrl } - ); - _prisma = prisma; - - const db = enhance(); - - await expect( - db.post.createManyAndReturn({ - data: [ - { - name: 'bla', - }, - { - name: 'blu', - }, - ], - }) - ).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'bla' }), - expect.objectContaining({ name: 'blu' }), - ]) - ); - - await expect( - db.post.updateManyAndReturn({ - data: { name: 'foo' }, - }) - ).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ name: 'foo' }), - expect.objectContaining({ name: 'foo' }), - ]) - ); - } finally { - await _prisma.$disconnect(); - await dropPostgresDb('issue-1955-2'); - } - }); -}); diff --git a/tests/regression/tests/issue-1964.test.ts b/tests/regression/tests/issue-1964.test.ts deleted file mode 100644 index e90aae32e..000000000 --- a/tests/regression/tests/issue-1964.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1964', () => { - it('regression1', async () => { - const { enhance } = await loadSchema( - ` -model User { - id Int @id - orgId String -} - -model Author { - id Int @id @default(autoincrement()) - orgId String - name String - posts Post[] - - @@unique([orgId, name]) - @@allow('all', auth().orgId == orgId) -} - -model Post { - id Int @id @default(autoincrement()) - orgId String - title String - author Author @relation(fields: [authorId], references: [id]) - authorId Int - - @@allow('all', auth().orgId == orgId) -} - `, - { - previewFeatures: ['strictUndefinedChecks'], - } - ); - - const db = enhance({ id: 1, orgId: 'org' }); - - const newauthor = await db.author.create({ - data: { - name: `Foo ${Date.now()}`, - orgId: 'org', - posts: { - createMany: { data: [{ title: 'Hello', orgId: 'org' }] }, - }, - }, - include: { posts: true }, - }); - - await expect( - db.author.update({ - where: { orgId_name: { orgId: 'org', name: newauthor.name } }, - data: { - name: `Bar ${Date.now()}`, - posts: { deleteMany: { id: { equals: newauthor.posts[0].id } } }, - }, - }) - ).toResolveTruthy(); - }); - - it('regression2', async () => { - const { enhance } = await loadSchema( - ` -model User { - id Int @id @default(autoincrement()) - slug String @unique - profile Profile? - @@allow('all', true) -} - -model Profile { - id Int @id @default(autoincrement()) - slug String @unique - name String - addresses Address[] - userId Int? @unique - user User? @relation(fields: [userId], references: [id]) - @@allow('all', true) -} - -model Address { - id Int @id @default(autoincrement()) - profileId Int @unique - profile Profile @relation(fields: [profileId], references: [id]) - city String - @@allow('all', true) -} - `, - { - previewFeatures: ['strictUndefinedChecks'], - } - ); - - const db = enhance({ id: 1, orgId: 'org' }); - - await db.user.create({ - data: { - slug: `user1`, - profile: { - create: { - name: `My Profile`, - slug: 'profile1', - addresses: { - create: { id: 1, city: 'City' }, - }, - }, - }, - }, - }); - - await expect( - db.user.update({ - where: { slug: 'user1' }, - data: { - profile: { - update: { - addresses: { - deleteMany: { id: { equals: 1 } }, - }, - }, - }, - }, - }) - ).toResolveTruthy(); - - await expect(db.address.count()).resolves.toEqual(0); - }); -}); diff --git a/tests/regression/tests/issue-1978.test.ts b/tests/regression/tests/issue-1978.test.ts deleted file mode 100644 index 63c91a6e5..000000000 --- a/tests/regression/tests/issue-1978.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1978', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id - posts Post[] - secret String @allow('read', posts?[published]) - @@allow('all', true) - } - - model Post { - id Int @id - author User @relation(fields: [authorId], references: [id]) - authorId Int - published Boolean @default(false) - @@allow('all', true) - } - `, - { logPrismaQuery: true } - ); - - const user1 = await prisma.user.create({ - data: { id: 1, secret: 'secret', posts: { create: { id: 1, published: true } } }, - }); - const user2 = await prisma.user.create({ - data: { id: 2, secret: 'secret' }, - }); - - const db = enhance(); - await expect(db.user.findFirst({ where: { id: 1 } })).resolves.toMatchObject({ secret: 'secret' }); - await expect(db.user.findFirst({ where: { id: 1 }, select: { id: true } })).resolves.toEqual({ id: 1 }); - - let r = await db.user.findFirst({ where: { id: 2 } }); - expect(r.secret).toBeUndefined(); - r = await db.user.findFirst({ where: { id: 2 }, select: { id: true } }); - expect(r.secret).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-1984.test.ts b/tests/regression/tests/issue-1984.test.ts deleted file mode 100644 index 7792e9e26..000000000 --- a/tests/regression/tests/issue-1984.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { loadModel, loadModelWithError, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1984', () => { - it('regression1', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - access String - - @@allow('all', - contains(auth().access, currentModel()) || - contains(auth().access, currentOperation())) - } - ` - ); - - const db1 = enhance(); - await expect(db1.user.create({ data: { access: 'foo' } })).toBeRejectedByPolicy(); - - const db2 = enhance({ id: 1, access: 'aUser' }); - await expect(db2.user.create({ data: { access: 'aUser' } })).toResolveTruthy(); - - const db3 = enhance({ id: 1, access: 'do-create-read' }); - await expect(db3.user.create({ data: { access: 'do-create-read' } })).toResolveTruthy(); - - const db4 = enhance({ id: 1, access: 'do-read' }); - await expect(db4.user.create({ data: { access: 'do-read' } })).toBeRejectedByPolicy(); - }); - - it('regression2', async () => { - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - modelName String - @@validate(contains(modelName, currentModel())) - } - ` - ) - ).resolves.toContain('function "currentModel" is not allowed in the current context: ValidationRule'); - }); - - it('regression3', async () => { - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - modelName String @contains(currentModel()) - } - ` - ) - ).resolves.toContain('function "currentModel" is not allowed in the current context: ValidationRule'); - }); -}); diff --git a/tests/regression/tests/issue-1991.test.ts b/tests/regression/tests/issue-1991.test.ts deleted file mode 100644 index da8443b6a..000000000 --- a/tests/regression/tests/issue-1991.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1991', () => { - it('regression', async () => { - await loadSchema( - ` - type FooMetadata { - isLocked Boolean - } - - type FooOptionMetadata { - color String - } - - model Foo { - id String @id @db.Uuid @default(uuid()) - meta FooMetadata @json - } - - model FooOption { - id String @id @db.Uuid @default(uuid()) - meta FooOptionMetadata @json - } - `, - { - provider: 'postgresql', - pushDb: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - - const prisma = new PrismaClient(); - const db = enhance(prisma); - - db.fooOption.create({ - data: { meta: { color: 'red' } } - }) - `, - }, - ], - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1992.test.ts b/tests/regression/tests/issue-1992.test.ts deleted file mode 100644 index 51715c3a5..000000000 --- a/tests/regression/tests/issue-1992.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1992', () => { - it('regression', async () => { - await loadSchema( - ` - enum MyAppUserType { - Local - Google - Microsoft - } - - model MyAppCompany { - id String @id @default(cuid()) - name String - users MyAppUser[] - - userFolders MyAppUserFolder[] - } - - model MyAppUser { - id String @id @default(cuid()) - companyId String - type MyAppUserType - - @@delegate(type) - - company MyAppCompany @relation(fields: [companyId], references: [id]) - userFolders MyAppUserFolder[] - } - - model MyAppUserLocal extends MyAppUser { - email String - password String - } - - model MyAppUserGoogle extends MyAppUser { - googleId String - } - - model MyAppUserMicrosoft extends MyAppUser { - microsoftId String - } - - model MyAppUserFolder { - id String @id @default(cuid()) - companyId String - userId String - path String - name String - - @@unique([companyId, userId, name]) - @@unique([companyId, userId, path]) - - company MyAppCompany @relation(fields: [companyId], references: [id]) - user MyAppUser @relation(fields: [userId], references: [id]) - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - }); -}); diff --git a/tests/regression/tests/issue-1993.test.ts b/tests/regression/tests/issue-1993.test.ts deleted file mode 100644 index 23561f8e4..000000000 --- a/tests/regression/tests/issue-1993.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1993', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` -enum UserType { - UserLocal - UserGoogle -} - -model User { - id String @id @default(cuid()) - companyId String? - type UserType - - @@delegate(type) - - userFolders UserFolder[] - - @@allow('all', true) -} - -model UserLocal extends User { - email String - password String -} - -model UserGoogle extends User { - googleId String -} - -model UserFolder { - id String @id @default(cuid()) - userId String - path String - - user User @relation(fields: [userId], references: [id]) - - @@allow('all', true) -} `, - { pushDb: false, fullZod: true, compile: true, output: 'lib/zenstack' } - ); - - expect( - zodSchemas.input.UserLocalInputSchema.create.safeParse({ - data: { - email: 'test@example.com', - password: 'password', - }, - }) - ).toMatchObject({ success: true }); - - expect( - zodSchemas.input.UserFolderInputSchema.create.safeParse({ - data: { - path: '/', - userId: '1', - }, - }) - ).toMatchObject({ success: true }); - }); -}); diff --git a/tests/regression/tests/issue-1994.test.ts b/tests/regression/tests/issue-1994.test.ts deleted file mode 100644 index e8fe40e62..000000000 --- a/tests/regression/tests/issue-1994.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1994', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` -model OrganizationRole { - id Int @id @default(autoincrement()) - rolePrivileges OrganizationRolePrivilege[] - type String - @@delegate(type) -} - -model Organization { - id Int @id @default(autoincrement()) - customRoles CustomOrganizationRole[] -} - -// roles common to all orgs, defined once -model SystemDefinedRole extends OrganizationRole { - name String @unique -} - -// roles specific to each org -model CustomOrganizationRole extends OrganizationRole { - name String - organizationId Int - organization Organization @relation(fields: [organizationId], references: [id]) - - @@unique([organizationId, name]) - @@index([organizationId]) -} - -model OrganizationRolePrivilege { - organizationRoleId Int - privilegeId Int - - organizationRole OrganizationRole @relation(fields: [organizationRoleId], references: [id]) - privilege Privilege @relation(fields: [privilegeId], references: [id]) - - @@id([organizationRoleId, privilegeId]) -} - -model Privilege { - id Int @id @default(autoincrement()) - name String // e.g. "org:manage" - - orgRolePrivileges OrganizationRolePrivilege[] - @@unique([name]) -} - `, - { - enhancements: ['delegate'], - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` - import { PrismaClient } from '@prisma/client'; - import { enhance } from '.zenstack/enhance'; - - const prisma = new PrismaClient(); - - async function main() { - const db = enhance(prisma); - const privilege = await db.privilege.create({ - data: { name: 'org:manage' }, - }); - - await db.systemDefinedRole.create({ - data: { - name: 'Admin', - rolePrivileges: { - create: [ - { - privilegeId: privilege.id, - }, - ], - }, - }, - }); - } - main() - `, - }, - ], - } - ); - - const db = enhance(); - - const privilege = await db.privilege.create({ - data: { name: 'org:manage' }, - }); - - await expect( - db.systemDefinedRole.create({ - data: { - name: 'Admin', - rolePrivileges: { - create: [ - { - privilegeId: privilege.id, - }, - ], - }, - }, - }) - ).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-1997.test.ts b/tests/regression/tests/issue-1997.test.ts deleted file mode 100644 index 3153c26c6..000000000 --- a/tests/regression/tests/issue-1997.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1997', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Tenant { - id String @id @default(uuid()) - - users User[] - posts Post[] - comments Comment[] - postUserLikes PostUserLikes[] - } - - model User { - id String @id @default(uuid()) - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - posts Post[] - likes PostUserLikes[] - - @@allow('all', true) - } - - model Post { - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - id String @default(uuid()) - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) - - comments Comment[] - likes PostUserLikes[] - - @@id([tenantId, id]) - - @@allow('all', true) - } - - model PostUserLikes { - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - id String @default(uuid()) - - userId String - user User @relation(fields: [userId], references: [id]) - - postId String - post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) - - @@id([tenantId, id]) - @@unique([tenantId, userId, postId]) - - @@allow('all', true) - } - - model Comment { - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - id String @default(uuid()) - postId String - post Post @relation(fields: [tenantId, postId], references: [tenantId, id]) - - @@id([tenantId, id]) - - @@allow('all', true) - } - `, - { logPrismaQuery: true } - ); - - const tenant = await prisma.tenant.create({ - data: {}, - }); - const user = await prisma.user.create({ - data: { tenantId: tenant.id }, - }); - - const db = enhance({ id: user.id, tenantId: tenant.id }); - - await expect( - db.post.create({ - data: { - likes: { - createMany: { - data: [ - { - userId: user.id, - }, - ], - }, - }, - }, - include: { - likes: true, - }, - }) - ).resolves.toMatchObject({ - authorId: user.id, - likes: [ - { - tenantId: tenant.id, - userId: user.id, - }, - ], - }); - - await expect( - db.post.create({ - data: { - comments: { - createMany: { - data: [{}], - }, - }, - }, - include: { - comments: true, - }, - }) - ).resolves.toMatchObject({ - authorId: user.id, - comments: [ - { - tenantId: tenant.id, - }, - ], - }); - }); -}); diff --git a/tests/regression/tests/issue-1998.test.ts b/tests/regression/tests/issue-1998.test.ts deleted file mode 100644 index a2810dfea..000000000 --- a/tests/regression/tests/issue-1998.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 1998', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model Entity { - id String @id - type String - updatable Boolean - children Relation[] @relation("children") - parents Relation[] @relation("parents") - - @@delegate(type) - @@allow('create,read', true) - @@allow('update', updatable) - } - - model A extends Entity {} - - model B extends Entity {} - - model Relation { - parent Entity @relation("children", fields: [parentId], references: [id]) - parentId String - child Entity @relation("parents", fields: [childId], references: [id]) - childId String - - @@allow('create', true) - @@allow('read', check(parent, 'read') && check(child, 'read')) - @@allow('delete', check(parent, 'update') && check(child, 'update')) - - @@id([parentId, childId]) - } - ` - ); - - const db = enhance(); - - await db.a.create({ data: { id: '1', updatable: true } }); - await db.b.create({ data: { id: '2', updatable: true } }); - await db.relation.create({ data: { parentId: '1', childId: '2' } }); - - await expect( - db.relation.deleteMany({ - where: { parentId: '1', childId: '2' }, - }) - ).resolves.toEqual({ count: 1 }); - - await db.a.create({ data: { id: '3', updatable: false } }); - await db.b.create({ data: { id: '4', updatable: false } }); - await db.relation.create({ data: { parentId: '3', childId: '4' } }); - await expect( - db.relation.deleteMany({ - where: { parentId: '3', childId: '4' }, - }) - ).resolves.toEqual({ count: 0 }); - }); -}); diff --git a/tests/regression/tests/issue-2000.test.ts b/tests/regression/tests/issue-2000.test.ts deleted file mode 100644 index 8f8e50b57..000000000 --- a/tests/regression/tests/issue-2000.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2000', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - abstract model Base { - id String @id @default(uuid()) @deny('update', true) - createdAt DateTime @default(now()) @deny('update', true) - updatedAt DateTime @updatedAt @deny('update', true) - active Boolean @default(false) - published Boolean @default(true) - deleted Boolean @default(false) - startDate DateTime? - endDate DateTime? - - @@allow('create', true) - @@allow('read', true) - @@allow('update', true) - } - - enum EntityType { - User - Alias - Group - Service - Device - Organization - Guest - } - - model Entity extends Base { - entityType EntityType - name String? @unique - members Entity[] @relation("members") - memberOf Entity[] @relation("members") - @@delegate(entityType) - - - @@allow('create', true) - @@allow('read', true) - @@allow('update', true) - @@validate(!active || (active && name != null), "Active Entities Must Have A Name") - } - - model User extends Entity { - profile Json? - username String @unique - password String @password - - @@allow('create', true) - @@allow('read', true) - @@allow('update', true) - } - ` - ); - - const db = enhance(); - await expect(db.user.create({ data: { username: 'admin', password: 'abc12345' } })).toResolveTruthy(); - await expect( - db.user.update({ where: { username: 'admin' }, data: { password: 'abc123456789123' } }) - ).toResolveTruthy(); - - // violating validation rules - await expect(db.user.update({ where: { username: 'admin' }, data: { active: true } })).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/regression/tests/issue-2007.test.ts b/tests/regression/tests/issue-2007.test.ts deleted file mode 100644 index 4a4b9cbe6..000000000 --- a/tests/regression/tests/issue-2007.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2007', () => { - it('regression1', async () => { - const { enhance } = await loadSchema( - ` - model Page { - id String @id @default(cuid()) - title String - - images Image[] - - @@allow('all', true) - } - - model Image { - id String @id @default(cuid()) @deny('update', true) - url String - pageId String? - page Page? @relation(fields: [pageId], references: [id]) - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - const image = await db.image.create({ - data: { - url: 'https://example.com/image.png', - }, - }); - - await expect( - db.image.update({ - where: { id: image.id }, - data: { - page: { - create: { - title: 'Page 1', - }, - }, - }, - }) - ).toResolveTruthy(); - }); - - it('regression2', async () => { - const { enhance } = await loadSchema( - ` - model Page { - id String @id @default(cuid()) - title String - - images Image[] - - @@allow('all', true) - } - - model Image { - id String @id @default(cuid()) - url String - pageId String? @deny('update', true) - page Page? @relation(fields: [pageId], references: [id]) - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - const image = await db.image.create({ - data: { - url: 'https://example.com/image.png', - }, - }); - - await expect( - db.image.update({ - where: { id: image.id }, - data: { - page: { - create: { - title: 'Page 1', - }, - }, - }, - }) - ).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/regression/tests/issue-2014.test.ts b/tests/regression/tests/issue-2014.test.ts deleted file mode 100644 index 4ebdb2b4e..000000000 --- a/tests/regression/tests/issue-2014.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2014', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Tenant { - id Int @id @default(autoincrement()) - - users User[] - } - - model User { - id Int @id @default(autoincrement()) - tenantId Int @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - - @@allow('all', true) - } - `, - { logPrismaQuery: true } - ); - - const tenant = await prisma.tenant.create({ data: {} }); - const user = await prisma.user.create({ data: { tenantId: tenant.id } }); - - const db = enhance(user); - const extendedDb = db.$extends({}); - - await expect( - extendedDb.user.create({ - data: {}, - }) - ).resolves.toEqual({ - id: 2, - tenantId: tenant.id, - }); - }); -}); diff --git a/tests/regression/tests/issue-2019.test.ts b/tests/regression/tests/issue-2019.test.ts deleted file mode 100644 index e5eea9254..000000000 --- a/tests/regression/tests/issue-2019.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2019', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Tenant { - id String @id @default(uuid()) - - users User[] - content Content[] - } - - model User { - id String @id @default(uuid()) - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - posts Post[] - likes PostUserLikes[] - - @@allow('all', true) - } - - model Content { - tenantId String @default(auth().tenantId) - tenant Tenant @relation(fields: [tenantId], references: [id]) - id String @id @default(uuid()) - contentType String - - @@delegate(contentType) - @@allow('all', true) - } - - model Post extends Content { - author User @relation(fields: [authorId], references: [id]) - authorId String @default(auth().id) - - comments Comment[] - likes PostUserLikes[] - - @@allow('all', true) - } - - model PostUserLikes extends Content { - userId String - user User @relation(fields: [userId], references: [id]) - - postId String - post Post @relation(fields: [postId], references: [id]) - - @@unique([userId, postId]) - - @@allow('all', true) - } - - model Comment extends Content { - postId String - post Post @relation(fields: [postId], references: [id]) - - @@allow('all', true) - } - `, - { logPrismaQuery: true } - ); - - const tenant = await prisma.tenant.create({ data: {} }); - const user = await prisma.user.create({ data: { tenantId: tenant.id } }); - const db = enhance({ id: user.id, tenantId: tenant.id }); - const result = await db.post.create({ - data: { - likes: { - createMany: { - data: [ - { - userId: user.id, - }, - ], - }, - }, - }, - include: { - likes: true, - }, - }); - expect(result.likes[0].tenantId).toBe(tenant.id); - }); -}); diff --git a/tests/regression/tests/issue-2025.test.ts b/tests/regression/tests/issue-2025.test.ts deleted file mode 100644 index 92a8b6388..000000000 --- a/tests/regression/tests/issue-2025.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2025', () => { - it('regression', async () => { - const { enhanceRaw, prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - email String @unique @email - termsAndConditions Int? - @@allow('all', true) - } - ` - ); - - const user = await prisma.user.create({ - data: { - email: 'xyz', // invalid email - }, - }); - - const db = enhanceRaw(prisma, undefined, { validation: { inputOnlyValidationForUpdate: true } }); - await expect( - db.user.update({ - where: { id: user.id }, - data: { - termsAndConditions: 1, - }, - }) - ).toResolveTruthy(); - - const db1 = enhanceRaw(prisma); - await expect( - db1.user.update({ - where: { id: user.id }, - data: { - termsAndConditions: 1, - }, - }) - ).toBeRejectedByPolicy(); - }); -}); diff --git a/tests/regression/tests/issue-2028.test.ts b/tests/regression/tests/issue-2028.test.ts deleted file mode 100644 index a98f0af7d..000000000 --- a/tests/regression/tests/issue-2028.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { createPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2028', () => { - it('regression', async () => { - const dbUrl = await createPostgresDb('issue-2028'); - const { enhance, zodSchemas } = await loadSchema( - ` -enum FooType { - Bar - Baz -} - -model User { - id String @id @default(cuid()) - userFolders UserFolder[] - @@allow('all', true) -} - -model Foo { - id String @id @default(cuid()) - type FooType - - userFolders UserFolder[] - - @@delegate(type) - @@allow('all', true) -} - -model Bar extends Foo { - name String -} - -model Baz extends Foo { - age Int -} - -model UserFolder { - id String @id @default(cuid()) - userId String - fooId String - - user User @relation(fields: [userId], references: [id]) - foo Foo @relation(fields: [fooId], references: [id]) - - @@unique([userId, fooId]) - @@allow('all', true) -} - `, - { - fullZod: true, - provider: 'postgresql', - dbUrl, - } - ); - // Ensure Zod Schemas don't include the delegate fields - expect( - zodSchemas.objects.UserFolderWhereUniqueInputObjectSchema.safeParse({ - userId_delegate_aux_UserFolder_fooId_Bar: { - userId: '1', - fooId: '2', - }, - }).success - ).toBeFalsy(); - - expect( - zodSchemas.objects.UserFolderWhereUniqueInputObjectSchema.safeParse({ - userId_delegate_aux_UserFolder_fooId_Baz: { - userId: '1', - fooId: '2', - }, - }).success - ).toBeFalsy(); - - // Ensure we can query by the CompoundUniqueInput - const db = enhance(); - const user = await db.user.create({ data: {} }); - const bar = await db.bar.create({ data: { name: 'bar' } }); - const baz = await db.baz.create({ data: { age: 1 } }); - - const userFolderA = await db.userFolder.create({ - data: { - userId: user.id, - fooId: bar.id, - }, - }); - - const userFolderB = await db.userFolder.create({ - data: { - userId: user.id, - fooId: baz.id, - }, - }); - - await expect( - db.userFolder.findUnique({ - where: { - userId_fooId: { - userId: user.id, - fooId: bar.id, - }, - }, - }) - ).resolves.toMatchObject(userFolderA); - - await expect( - db.userFolder.findUnique({ - where: { - userId_fooId: { - userId: user.id, - fooId: baz.id, - }, - }, - }) - ).resolves.toMatchObject(userFolderB); - }); -}); diff --git a/tests/regression/tests/issue-2038.test.ts b/tests/regression/tests/issue-2038.test.ts deleted file mode 100644 index 02218eaab..000000000 --- a/tests/regression/tests/issue-2038.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2038', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - flag Boolean - @@allow('all', true) - } - - model Post { - id Int @id @default(autoincrement()) - published Boolean @default(auth().flag) - @@allow('all', true) - } - ` - ); - - const db = enhance({ id: 1, flag: true }); - await expect(db.post.create({ data: {} })).resolves.toMatchObject({ - published: true, - }); - }); -}); diff --git a/tests/regression/tests/issue-2039.test.ts b/tests/regression/tests/issue-2039.test.ts deleted file mode 100644 index bddca560a..000000000 --- a/tests/regression/tests/issue-2039.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2039', () => { - it('regression', async () => { - const dbUrl = await createPostgresDb('issue-2039'); - const { zodSchemas, enhance } = await loadSchema( - ` -type Foo { - a String -} - -model Bar { - id String @id @default(cuid()) - foo Foo @json @default("{ \\"a\\": \\"a\\" }") - fooList Foo[] @json @default("[{ \\"a\\": \\"b\\" }]") - @@allow('all', true) -} - `, - { - fullZod: true, - provider: 'postgresql', - dbUrl, - } - ); - - // Ensure default values are correctly set - const db = enhance(); - await expect(db.bar.create({ data: {} })).resolves.toMatchObject({ - id: expect.any(String), - foo: { a: 'a' }, - fooList: [{ a: 'b' }], - }); - - // Ensure Zod Schemas are correctly generated - expect( - zodSchemas.objects.BarCreateInputObjectSchema.safeParse({ - foo: { a: 'a' }, - fooList: [{ a: 'a' }], - }).success - ).toBeTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-2065.test.ts b/tests/regression/tests/issue-2065.test.ts deleted file mode 100644 index cf186b4fe..000000000 --- a/tests/regression/tests/issue-2065.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue [...]', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` - enum FooType { - Bar - Baz - } - - type Meta { - test String? - } - - model Foo { - id String @id @db.Uuid @default(uuid()) - type FooType - meta Meta @json - - @@validate(type == Bar, "FooType must be Bar") - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - expect(zodSchemas.models.FooSchema).toBeTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-2106.test.ts b/tests/regression/tests/issue-2106.test.ts deleted file mode 100644 index c81347918..000000000 --- a/tests/regression/tests/issue-2106.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2106', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id - age BigInt - @@allow('all', true) - } - `, - { logPrismaQuery: true } - ); - - const db = enhance(); - await expect(db.user.create({ data: { id: 1, age: 1n } })).toResolveTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-2117.test.ts b/tests/regression/tests/issue-2117.test.ts deleted file mode 100644 index 46be7c195..000000000 --- a/tests/regression/tests/issue-2117.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2117', () => { - it('regression', async () => { - const { prisma, enhanceRaw, prismaModule } = await loadSchema( - ` - model User { - uuid String @id - email String @unique @deny('read', auth().uuid != this.uuid) - username String @unique - @@allow('all', true) - } - ` - ); - - const extPrisma = prisma.$extends( - prismaModule.defineExtension({ - name: 'urls-extension', - result: { - user: { - pageUrl: { - needs: { username: true }, - compute: () => `foo`, - }, - }, - }, - }) - ); - - const db = enhanceRaw(extPrisma, { user: { uuid: '1' } }, { logPrismaQuery: true }); - await db.user.create({ data: { uuid: '1', email: 'a@b.com', username: 'a' } }); - await expect(db.user.findFirst()).resolves.toMatchObject({ - uuid: '1', - email: 'a@b.com', - username: 'a', - pageUrl: 'foo', - }); - const r = await db.user.findFirst({ select: { email: true } }); - expect(r.email).toBeTruthy(); - expect(r.uuid).toBeUndefined(); - expect(r.pageUrl).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-2168.test.ts b/tests/regression/tests/issue-2168.test.ts deleted file mode 100644 index b588e1a60..000000000 --- a/tests/regression/tests/issue-2168.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2168', () => { - it('regression', async () => { - await loadSchema( - ` - datasource db { - provider = "sqlite" - url = "file:./test.db" - - } - - generator client { - provider = "prisma-client" - output = "../generated/prisma" - moduleFormat = "cjs" - } - - model User { - id Int @id - profile Profile @json - } - - type Profile { - age Int - } - `, - { - compile: true, - addPrelude: false, - output: './generated/zenstack', - prismaLoadPath: './generated/prisma/client', - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import type { Profile } from './generated/zenstack/models'; -const profile: Profile = { age: 18 }; -console.log(profile); -`, - }, - ], - } - ); - }); -}); diff --git a/tests/regression/tests/issue-2175.test.ts b/tests/regression/tests/issue-2175.test.ts deleted file mode 100644 index 7b31aa989..000000000 --- a/tests/regression/tests/issue-2175.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2175', () => { - it('regression standard generator', async () => { - await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - title String - author User? @relation(fields: [authorId], references: [id]) - authorId Int? @default(auth().id) - } - - `, - { - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient } from "@prisma/client"; -import { enhance } from ".zenstack/enhance"; - -const prisma = new PrismaClient(); -const prismaExtended = prisma.$extends({ - model: { - user: { - async signUp(email: string) { - return prisma.user.create({ data: { email } }); - }, - }, - }, -}); - -const dbExtended = enhance(prismaExtended); - -async function main() { - const newUser = await dbExtended.user.signUp("a@b.com"); - console.log(newUser); -} - -main(); -`, - }, - ], - } - ); - }); - - it('regression new generator', async () => { - await loadSchema( - ` - datasource db { - provider = "sqlite" - url = "file:./test.db" - } - - generator client { - provider = "prisma-client" - output = "../generated/prisma" - moduleFormat = "cjs" - } - - model User { - id Int @id @default(autoincrement()) - email String @unique - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - title String - author User? @relation(fields: [authorId], references: [id]) - authorId Int? @default(auth().id) - } - - `, - { - addPrelude: false, - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { PrismaClient } from "./generated/prisma/client"; -import { enhance } from "./generated/zenstack/enhance"; - -const prisma = new PrismaClient(); -const prismaExtended = prisma.$extends({ - model: { - user: { - async signUp(email: string) { - return prisma.user.create({ data: { email } }); - }, - }, - }, -}); - -const dbExtended = enhance(prismaExtended); - -async function main() { - const newUser = await dbExtended.user.signUp("a@b.com"); - console.log(newUser); -} - -main(); -`, - }, - ], - output: './generated/zenstack', - prismaLoadPath: './generated/prisma/client', - } - ); - }); -}); diff --git a/tests/regression/tests/issue-2226.test.ts b/tests/regression/tests/issue-2226.test.ts deleted file mode 100644 index 8dc55bfad..000000000 --- a/tests/regression/tests/issue-2226.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2226', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` -model Registration { - id String @id - regType String - @@delegate(regType) - - replacedRegistrationId String? - replacedRegistration Registration? @relation("ReplacedBy", fields: [replacedRegistrationId], references: [id]) - replacements Registration[] @relation("ReplacedBy") -} - -// Delegated subtype -model RegistrationFramework extends Registration { -} -`, - { fullZod: true } - ); - - const schema = zodSchemas.objects.RegistrationFrameworkUpdateInputObjectSchema; - expect(schema).toBeDefined(); - const parsed = schema.safeParse({ - replacedRegistrationId: '123', - }); - expect(parsed.success).toBe(true); - }); -}); diff --git a/tests/regression/tests/issue-2246.test.ts b/tests/regression/tests/issue-2246.test.ts deleted file mode 100644 index ab487e6d9..000000000 --- a/tests/regression/tests/issue-2246.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue 2246', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` -model Media { - id Int @id @default(autoincrement()) - title String - mediaType String - - @@delegate(mediaType) - @@allow('all', true) -} - -model Movie extends Media { - director Director @relation(fields: [directorId], references: [id]) - directorId Int - duration Int - rating String -} - -model Director { - id Int @id @default(autoincrement()) - name String - email String - movies Movie[] - - @@allow('all', true) -} - ` - ); - - const db = enhance(); - - await db.director.create({ - data: { - name: 'Christopher Nolan', - email: 'christopher.nolan@example.com', - movies: { - create: { - title: 'Inception', - duration: 148, - rating: 'PG-13', - }, - }, - }, - }); - - await expect( - db.director.findMany({ - include: { - movies: { - where: { title: 'Inception' }, - }, - }, - }) - ).resolves.toHaveLength(1); - - await expect( - db.director.findFirst({ - include: { - _count: { select: { movies: { where: { title: 'Inception' } } } }, - }, - }) - ).resolves.toMatchObject({ _count: { movies: 1 } }); - - await expect( - db.movie.findMany({ - where: { title: 'Interstellar' }, - }) - ).resolves.toHaveLength(0); - - await expect( - db.director.findFirst({ - include: { - _count: { select: { movies: { where: { title: 'Interstellar' } } } }, - }, - }) - ).resolves.toMatchObject({ _count: { movies: 0 } }); - }); -}); diff --git a/tests/regression/tests/issue-2283/dev.db b/tests/regression/tests/issue-2283/dev.db deleted file mode 100644 index 8eab9f73677d208cf335d442f88624bb4d05da19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 208896 zcmeI*e{37qVF&O#QW8a3_PKSGtK+yiQE_D^W=x8rNTSFTiaKjdBub&kE?Jv0$Kz2t zo4?E>EjjHUQ>-LTk$?8b{^|Z0h7D-{SleOP9|czAuNCN84A?&df&l~8=D+Qa0xeLi zCkr#VVOJ>2hT9D(}-_q$WSo&3cAol`#71C#i$XrNv?>Y#x+oxAo)E|*Ah#5$t&~&k zfUr#B*?f_PZst>K8&`~#JI(UImCYz>IUwZ4 zH%Nk3q8qu)S}OOZ@S6Ch8K*AQNP!%&%n)t6(P}R%n%q*GZM9K1Vwr)}Mty5wm{iL2 z=4`Ik?3Aji91xb)*H^_<_Ml+a)>fpdrdW|%YNMsLZwG`-HZQJ-x&G*3GpefP6{%+6 z4wH6zJ-eCDrAWF)l@zPA%*DzLh2(Z^Jtt;XvJ^?U9MD4oK_Mqz6G=s+#Z4hV8UU%p z0Pzh9>seA&t0Jl5bZRr5S{CV&!UnBSYso}#_Ust{)u};kik7<=q0e8qvvb|l69r}jbkDv1K)6?wTq~2gi z%iL%+Dr!|Zcs1#4E(ZZe)>?+?h;(1~o~WcY9G#C^*V@unOR71RHjVAx*lx5NR?{@Q z2d&!Kvd6Wzumg@q+X?iyE?dsdvb#2MXOx}}DN~cS=y9dDOWG98qlrtO6rCL7SEdK` zNw&g84ql($yXE)s=gzTrHm&AH0w?+!Shr>A9lo}$kmJEH{qk`=Bu)e`O^or$bBAaZ zDbuO_t3JK|zI&uC z6rKCn(d>q?@q6|AR_XN9P4kU^uQaNkZr+*gHZnR(}>eUUv{iOw05IK`tF=k8B)bO zq?J1@iJq0Osnx2g^`9b}(RS2wd%GKIF5Fhst!*;sFi)$^rJDK??XL=m#bJ$>XdA>u7a+MnRvo7M#3YHFxLU;6dP? z!@zaBVub!7qv@#&|9r`o7W^mO**d;YAF@-3ZS4* zbTB_-em25PknLi*PZ{zL4+ua20uX=z1Rwwb2tWV=5P$##POiYS-bMewYXrvp|MwW~ zdnb1TQE~`C00Izz00bZa0SG_<0uX?}VtyObEYrFF zMx~`ScPd)5rQA}LozN}%eHLTxUtb{T-?w1?`)%K!jU>)8nE!uF8VEK60uX=z1Rwwb z2tWV=5P$##o_GP<{D1#-0y_Uca^@cx?g!kTa--x09uR;41Rwwb2tWV=5P$##AOL}5 zA@KUh*yw2C>a;!q{>R31DXPTE;aDtGSxAIK^U-iJl#E6zA*Gy5&MnL()zBCgDZlw>@dtiD=Op?NtO4<+MKIaCTq z1$LNA6C*U@Fc%!WG~zH95j?Zrf`6Dx1IGM+g5kc!ojvn6XA&og7iEP21Rwwb2tWV= z5P$##AOL|!FQAT$lP{zfg42v+(%ifi{SNs$dYpb8U2bYOqVYs!m{G7XAKxMK@dEjc z&{y>Ndiw5$SI_Fj8P}|Ck*pSIRyTG^H!6u(a+q-Kpa zl39#qcJpSeS)W(04>J}w=I^_C>GS*a-JXXv^I9~f^7ZFsE3X>KD@^l}O3hm9rdk_j zIB!hsck|LG{^>g_(Z1gZFy{Y1B=`U4e#(81`|#1X9OM822tWV=5P$##AOHafKmY;| zc!C8kdKX!1l-_swWW*lYcg4eN5AC}|;jw-Hzt(qA!MOkbFBt9@Pp~~U9Rd)500bZa z0SG_<0uX=z1R!v71fuMipL{0cqW&=g(RjA5D748(3FhdB391n}cC#T@@`~0LR;3c1 z|7W>R$^8Gx*-(@e0uX=z1Rwwb2tWV=5P$##AOL|r0sH)a-*o_E{{Pzy_wBy*umA!O zfB*y_009U<00Izz00bZafrlWl$Fg+7e(}=9#o0lV_VaYozFceV*5j?x;j{TwsifaH zp-rat$!jZe-&g)=c!)#@Y1Pga=Kmi;MIs3ZKmY;|fB*y_009U<00Izzz~?W}JOA&v zq(JBYz1&%b`+M$f@&^wHKmY;|fB*y_009U<00Izzz!M>m^^T5?7K|J6zot*hzc$Qo z7g&?+eV?z-div5E<_8J}r^gG)@40!b<9_V6#Cq+u(f1vR{o&Wc6-=y>Z-yC}5 z5fK6qfB*y_009U<00Izz00bZa0h_>v=M?#Z|9OU49B0Rk*?QyInvdTl_vJ6?_vPk|U_rmerK$Hvu5P$##AOHafKmY;|fB*y_a8v@S$4BoBu*m51?W@M~1ATmd;E-Kq zU9x(hkNMlPa(p_#nEzj4xWDCAj!F{wKmY;|fB*y_009U<00Izz00d5`z&GfX0rt^c z4e)%OCjMYw^B*|=Kh6CwbC%s_&V0cA>uL7PH%|R_@)Q4ePW@#3x?djs!+30j_5Pgw z0lUwxv1=!^ZjP?lu`iDCnHiQ*>t$tEyIED+N>S>x8~XcVK~q|*QYqJ{D#eKH&AHjL zWBgaA2CZhqr_W#5e}?n%Q&a3+zAcri3SCD3pH1h)R9+PFsijp>2+&0V;qrt)9s#+f zNNuH@Y6pa65+|Qo6NT)0UdR?!SA}KqTB@*`7t)1XPR!J zQOf}#FTOz%v=ZIOW!6%;H-*>4H_bS8sYVLqh-HRo+l^LxS<&Q{+H9+hx)I9^tTyUf z1H+_JrZ;DEwPvSORpo%Nw7$M7rm_bGv$nP(RW-$m+)^7YwS7AvWU_g2Ma=a_51Ua{ zEw4y519zCT)9cyId@e=OHL9dYesVKYE>>&yuQI6-gDRQ=943vPhQ{HfV)fOD2M`Geb1KS4ms!Ha_}>yR&oJ z$Ir~LAH?)FWwuKiWP5qW*}`lgN7m3fWbYYN<%VBx3)9=Mtu+!&Gm$HTMQ+rsy{c^6 zRy3l!R+*WSv(D(c_fu(y=l(F-OOEW%xlB6m)?oJIr+obMGNwWlw2oDqQfiV~b?@cNs@AMZx3k^z%!%Oa*kR|? zbbF1{Am6)hczrz2v+r@b#@E!c($JqqoF4kJV+EqM8!ggz=akBjD&`@r+-XVlWP44m zR#mP4%-4*zqn6v--B5GkwxVurld*((wrnod)OW}sWGS;kPj;<=z(g=OGRD8m59;`I z&|*aYGx~0t)oWp2(Q83JDCtigXDwJqtK&1c`K-0zS1h8}W8WHuX_4A-|BEi%BQqiu6xv!&cpl^uOYvobbOGeoJPI0E1SgtnIh;{?3S%8EdW&+{X?5tCF+O!+P=9G| zUNK_6xcGkHypO+df&J)$zR#J9%|FjN_d0XwvD)pbgRkKn;U0PKovNpv?Yq{Y`>TEL zGX0NwoM>fh+iP8$vMs!)9a{(7;oEWlE@hR%3F%r8XOy0@e3pGG^`$YsaCOi=MX%;< zwA5SVqQ{nETpR0KbYXvm_wiS+vUgwB_p`nTeG8s(?t6XVN46KX$mUr+e&4~w+U!9Q zoNfEcgv;eLGktP7t!Yaw*xv&B2#Ut4u5G|f)p==+rr^@E54;{aOc`v2b}fi4yr(60 z+syqGX(E_>dW>J29n=`^YoODpPgmjyP-EiM_k97FF6~Z zBkaf;p4xkDUa(z_Ln%hO zFZnxPKj-70dyf5JRIkf!K=;Md&N}QaKPmNh$i$8Nj8AXpbVv)hpXnWYqv#?&>IU%{ zCjs}{U(hfAeRx%G6!Z!OX~(qNv%UPHvtigmj;v{PNWlgjG7hOWvr~7yBB<;()s~{! z=O|qdH)ht|*&Fi0I#%>|_cj~r(x6^E*PgyNW#_TSOp2Z#(Eh$Z@(}ZLhZoQ&nXxgh z1vF%B8|;`Xq^*ROtQ(8;zeCO%((ylY_Rz6C0wDka2tWV=5P$##AOHafKmY;|IN<{H z`~MjKpYTdV@gV>K2tWV=5P$##AOHafKmY>w5WxKZJ%A7l0SG_<0uX=z1Rwwb2tWV= z5O@p(F#rD;R3bJ30uX=z1Rwwb2tWV=5P$##AaD->%>Um52*D7500bZa0SG_<0uX=z z1Rwx`$3Ot{|Bpc>ViO<$0SG_<0uX=z1Rwwb2tWV=_Yk1-|12jlS z`8ioDE0SPXrYSZmEv>mz(dhg?%YDj_e|SIu0uX=z1Rwwb2tWV=5P$##AaHU8p7k#J zt?_@~r3PdE|80hQ`{ZsQN)7=CKmY;|fB*y_009U<00I#Byao0yvZu-PKb`$w^!u4b zeda%FJlhB5TXa%>LE3$%y&Ip`r}@_!KXQeZzb!E6o3Y-VFlz zLjVF0fB*y_009U<00Izz00bV5K=1s&<5B~i|9{F8VkXn$znT1!@2^IGGy0dK&yW1$ z$cpzCT$M^2N~@-7 z8u^MzzOm6#Z>d#fODQfD504N{=fqTA6f)Um@eLtx&x8WPdiIcLfy;rVKydHHmwfyt zDc$RbXI@M#6+3nHW=9#A@0?k_LN@byVTfXOQ@nRUQ-Lc2E$#MKe0+3@-J8-2n`=~+ zVqK~!#i~@wl#4f%+jP+juH|rvY~)J=hy_iD=AtJR2$nAS_^TwR(+4>jnW)`-f`jw1 zM|I?4q)xI?%YopH=gHPgvwNJL$0}(Ttwx2kg`!sNY|*VbSajJnuY>UJDn@Px0c6W) zYJp&V#>dAyXz*ZP}K7 z#WhP?gyO+!My9qfvXMq;AXu0lr)HSta1GRY_?<(bU*3TW;(Sj(j|qBW!fQ`ON&{qah_>BmwU-aR?rEfD238yOtwCY! z?M|&!C%sC>%->wcm|C(HcDK|FZM4c%o$RCHikRySZFWjkRohlbic9P3t70nK8{?K* zRvK2~W|}e)+G@1OK27@yW1)Qq)T^UtE3fNrXAAGy0W&peOL1u_o1)Pv4bur2Ww2~q z3t$EIp-o$gw-GX)YJzt~TE_n~$^od~b%VYe@Jj>_@AMIvU zZ7W5o({AYRiv~o**hOA^c8p&)fuV(yK$ zQ^an^ps#Ek+Yb6ca)cwDZsUq^ZeW~N9fUeh`izvqW)}Ttd1g*7r*%dM$;&#A+k5&& za!$;%dz*Tx3>kd~DOydsXf+CY=++}#bl&MEs9z2kdm%Xiv+|+md{$QEbok;6 z-+O3&&>NgOdqd3sLIzdh^YUw)3=3+komo+tDq)m{FgYbm@au{`vpD>kP*H|HlmX@iE>46b%9pfB*y_009U<00Izz00bcLL<_vadKU}k z+|MS>yFg6eZ5P$##AOHafKmY;|fB*y_ zaEt|}yjR(hq^a^N%C1yvR+X3JMs4J2Z-lKWwUW}ZFZR;NQn{wq?MppRd9V5#J9TGV zWBmUX!+pa2haXXb^w^1Rwwb2tWV=5P$##AOL~mDR9|aU`tUYRu0Evp~^xc z9GZ`Ylc8iZS_vuTWO8m{E-5FH3nMh5JTEPT6VgJctjy1c=EI4FP@cUe#R{P7 zC27Fit*9j3h?(R6A2Zy4az8$r4MZ*wfB*y_009U<00Izz00bZa0SF8$@chX6MRrZU z^`AAi?;G@c0wesQKSgf@I4};K|HtqD4XciL5P$##AOHafKmY;|fB*y_0D+S!fbsvy ztT&Vz0uX=z1Rwwb2tWV=5P$##AaFkd#{K_(#c;p6pS2Jd0uX=z1Rwwb2tWV=5P$## zAOL~mC2(oX1xXfu;YLH{@=-~Uz8XE5P$##AOHafKmY;|fB*y_a6bZc{vYH2`w>H22tWV= V5P$##AOHafKmY;|fWXNU_ { - it('regression', async () => { - const { enhance } = await loadSchema( - ` -// Base models -abstract model Base { - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() -} - -abstract model BaseWithCuid extends Base { - id String @id @default(cuid()) -} - -abstract model Publishable { - published Boolean @default(false) -} - -// Media models -model Image extends BaseWithCuid { - storageRef String - displayName String? - width Int - height Int - size BigInt - - // Relations - userProfiles UserProfile[] - labProfiles LabProfile[] - contents Content[] - modules Module[] - classes Class[] - - @@allow('all', true) -} - -model Video extends BaseWithCuid { - storageRef String - displayName String? - durationMillis Int - width Int? - height Int? - size BigInt - - // Relations - previewForContent Content[] - previewForModule Module[] - classes Class[] - - @@allow('all', true) -} - -// User models -model User extends Base { - id String @id @default(uuid()) - email String @unique - displayName String? - - profile UserProfile? - labs UserLabJoin[] - ownedLabs Lab[] - - @@allow('all', true) -} - -model UserProfile extends BaseWithCuid { - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String @unique - bio String? - instagram String? - profilePhoto Image? @relation(fields: [profilePhotoId], references: [id], onDelete: SetNull) - profilePhotoId String? - - @@allow('all', true) -} - -// Lab models -model Lab extends BaseWithCuid, Publishable { - name String - profile LabProfile? - owners User[] - community UserLabJoin[] - roles Role[] - privileges Privilege[] - content Content[] - permissions LabPermission[] - - @@allow('create', auth() != null) - @@allow('read', owners?[id == auth().id] || published) - @@allow('update', - owners?[id == auth().id] - || - community?[ - userLabRoles?[ - userId == auth().id - && - role.privileges?[ - privilege.labPermissions?[ - type == "ALLOW_ADMINISTRATION" - ] - ] - ] - ] - ) - @@allow('delete', owners?[id == auth().id]) -} - -model LabProfile extends BaseWithCuid { - lab Lab @relation(fields: [labId], references: [id], onDelete: Cascade) - labId String @unique - bio String? - instagram String? - profilePhoto Image? @relation(fields: [profilePhotoId], references: [id], onDelete: SetNull) - profilePhotoId String? - slug String? @unique - - @@allow('read', check(lab, "read")) - @@allow('create', lab.owners?[id == auth().id]) - @@allow('update', check(lab, "update")) - @@allow('delete', check(lab, "delete")) -} - -// User-Lab relationship -model UserLabJoin extends Base { - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - lab Lab @relation(fields: [labId], references: [id], onDelete: Restrict) - labId String - userLabRoles UserLabRole[] - - @@id(name: "userLabJoinId", [userId, labId]) - - @@allow('create', auth().id == userId) - @@allow('update', auth().id == userId) - @@allow('read', true) - @@allow('delete', auth().id == userId) -} - -// Role and Permission models -model Role extends BaseWithCuid { - name String - shortDescription String? - longDescription String? - lab Lab @relation(fields: [labId], references: [id], onDelete: Cascade) - labId String - userLabRoles UserLabRole[] - privileges RolePrivilegeJoin[] - public Boolean @default(false) - priority Int @default(0) - isTeamRole Boolean @default(false) - - @@unique([labId, id]) - @@unique([name, labId]) - - @@allow('read', - auth().id != null - && - ( - userLabRoles?[userId == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - && - labId == this.labId - ] - ] - || - lab.owners?[id == auth().id] - ) - ) - @@allow('create', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - && - privilege.labId == this.labId - ] - ] - ] - ) - ) - @@allow('update', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - && - privilege.labId == this.labId - ] - ] - ] - ) - ) - @@allow('delete', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - && - privilege.labId == this.labId - ] - ] - ] - ) - ) -} - -model UserLabRole extends Base { - userLabJoin UserLabJoin @relation(fields: [userId, labId], references: [userId, labId], onDelete: Cascade) - userId String - labId String - role Role @relation(fields: [labId, roleId], references: [labId, id], onDelete: Cascade) - roleId String - expiresAt DateTime? - - @@id(name: "userLabRoleId", [userId, labId, roleId]) - - @@allow('read', auth().id != null) - @@allow('create', - auth().id != null - && - ( - userLabJoin.lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) - @@allow('update', - auth().id != null - && - ( - userLabJoin.lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) - @@allow('delete', - auth().id != null - && - ( - userLabJoin.lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) -} - -model Privilege extends BaseWithCuid { - name String - longDescription String? - shortDescription String - lab Lab @relation(fields: [labId], references: [id], onDelete: Cascade) - labId String - roles RolePrivilegeJoin[] - labPermissions LabPermission[] - public Boolean @default(false) - - @@unique([name, labId]) - - @@allow('read', auth().id != null) - @@allow('create', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) - @@allow('update', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) - @@allow('delete', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) -} - -model LabPermission extends BaseWithCuid { - name String - lab Lab @relation(fields: [labId], references: [id], onDelete: Cascade) - labId String - privileges Privilege[] - type String - - @@unique([name, labId]) - - @@allow('read', auth().id != null) - @@allow('create', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == this.labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) - @@allow('update', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == this.labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) - @@allow('delete', - auth().id != null - && - ( - lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.labId == this.labId - && - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) -} - -model RolePrivilegeJoin extends Base { - role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) - roleId String - privilege Privilege @relation(fields: [privilegeId], references: [id], onDelete: Cascade) - privilegeId String - order Int? - - @@id(name: "rolePrivilegeJoinId", [roleId, privilegeId]) - - @@allow('read', auth().id != null) - @@allow('create', - auth().id != null - && - ( - role.lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) - @@allow('update', - auth().id != null - && - ( - role.lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) - @@allow('delete', - auth().id != null - && - ( - role.lab.owners?[id == auth().id] - || - auth().labs?[ - userLabRoles?[ - role.privileges?[ - privilege.labPermissions?[type == "ALLOW_ADMINISTRATION"] - ] - ] - ] - ) - ) -} - -// Content models -model Content extends BaseWithCuid { - lab Lab @relation(fields: [labId], references: [id], onDelete: Cascade) - labId String - name String - shortDescription String? - longDescription String? - thumbnail Image? @relation(fields: [thumbnailId], references: [id]) - thumbnailId String? - modules Module[] - published Boolean - previewVideo Video? @relation(fields: [previewVideoId], references: [id]) - previewVideoId String? - order Int - - @@unique([labId, order]) - - @@allow('read', - lab.owners?[id == auth().id] - || - lab.community?[ - userId == auth().id - && - userLabRoles?[ - labId == this.labId - && - role.privileges?[ - privilege.labPermissions?[ - type in ["ALLOW_ADMINISTRATION"] - ] - ] - ] - ] - || - published == true - ) - @@allow('create', - lab.owners?[id == auth().id] - || - lab.community?[ - userId == auth().id - && - userLabRoles?[ - labId == this.labId - && - role.privileges?[ - privilege.labPermissions?[ - type in ["ALLOW_ADMINISTRATION"] - ] - ] - ] - ] - ) - @@allow('update', - lab.owners?[id == auth().id] - || - lab.community?[ - userId == auth().id - && - userLabRoles?[ - labId == this.labId - && - role.privileges?[ - privilege.labPermissions?[ - type in ["ALLOW_ADMINISTRATION"] - ] - ] - ] - ] - ) - @@allow('delete', - lab.owners?[id == auth().id] - || - lab.community?[ - userId == auth().id - && - userLabRoles?[ - labId == this.labId - && - role.privileges?[ - privilege.labPermissions?[ - type in ["ALLOW_ADMINISTRATION"] - ] - ] - ] - ] - ) -} - -model Module extends BaseWithCuid { - name String - shortDescription String? - longDescription String? - thumbnail Image? @relation(fields: [thumbnailId], references: [id]) - thumbnailId String? - content Content @relation(fields: [contentId], references: [id], onDelete: Restrict) - contentId String - classes Class[] - order Int - published Boolean - category String? - previewVideo Video? @relation(fields: [previewVideoId], references: [id]) - previewVideoId String? - - @@unique([order, category, contentId]) - - @@allow('read', - content.lab.owners?[id == auth().id] - || - content.lab.permissions?[ - privileges?[ - roles?[ - role.userLabRoles?[ - userId == auth().id - ] - ] - && - labPermissions?[ - type in ["ALLOW_ADMINISTRATION"] - ] - ] - ] - || - ( - check(content, 'read') - && - published == true - ) - ) - @@allow('create', check(content, 'create')) - @@allow('update', check(content, 'update')) - @@allow('delete', check(content, 'delete')) -} - -model Class extends BaseWithCuid { - name String - shortDescription String? - longDescription String? - thumbnail Image? @relation(fields: [thumbnailId], references: [id]) - thumbnailId String? - module Module @relation(fields: [moduleId], references: [id], onDelete: Restrict) - moduleId String - order Int - published Boolean - video Video? @relation(fields: [videoId], references: [id]) - videoId String? - category String? - - @@unique([order, category, moduleId]) - - @@allow('read', check(module, 'read')) - @@allow('create', check(module, 'create')) - @@allow('update', check(module, 'update')) - @@allow('delete', check(module, 'delete')) -} -`, - { - dbFile: path.join(__dirname, 'dev.db'), - } - ); - - const db = enhance(); - - const r = await db.labProfile.findUnique({ - where: { - slug: 'test-lab-slug', - lab: { - published: true, - }, - }, - select: { - lab: { - select: { - id: true, - name: true, - content: { - where: { - published: true, - }, - select: { - id: true, - name: true, - modules: { - select: { - id: true, - name: true, - classes: { - select: { - id: true, - name: true, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }); - - expect(r.lab.content[0].modules[0].classes[0].module).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-2291.test.ts b/tests/regression/tests/issue-2291.test.ts deleted file mode 100644 index 58b73ea5d..000000000 --- a/tests/regression/tests/issue-2291.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Issue 2291', () => { - it('should work', async () => { - const { zodSchemas } = await loadSchema( - ` -enum SomeEnum { - Ex1 - Ex2 -} - -/// Post model -model Post { - id String @id @default(cuid()) - e SomeEnum @default(Ex1) -} -`, - { fullZod: true } - ); - - expect(zodSchemas.models.PostSchema.parse({ id: '1' })).toEqual({ id: '1', e: 'Ex1' }); - }); -}); diff --git a/tests/regression/tests/issue-2294.test.ts b/tests/regression/tests/issue-2294.test.ts deleted file mode 100644 index 8e16db7f3..000000000 --- a/tests/regression/tests/issue-2294.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Issue 2294', () => { - it('should work', async () => { - await loadSchema( - ` -datasource db { - provider = "sqlite" - url = "file:./dev.db" -} - -generator js { - provider = "prisma-client" - output = "./generated/client" - moduleFormat = "cjs" -} - -type AuthUser { - id Int @id - name String? - - @@auth -} - -model Foo { - id Int @id - @@allow('all', auth().name == 'admin') -} -`, - { - addPrelude: false, - output: './zenstack', - prismaLoadPath: './prisma/generated/client/client', - compile: true, - extraSourceFiles: [ - { - name: 'main.ts', - content: ` -import { enhance } from "./zenstack/enhance"; -import { PrismaClient } from './prisma/generated/client/client'; -enhance(new PrismaClient(), { user: { id: 1, name: 'admin' } }); - `, - }, - ], - } - ); - }); -}); diff --git a/tests/regression/tests/issue-416.test.ts b/tests/regression/tests/issue-416.test.ts deleted file mode 100644 index d911effd9..000000000 --- a/tests/regression/tests/issue-416.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 416', () => { - it('regression', async () => { - await loadSchema( - ` -model Example { - id Int @id - doubleQuote String @default("s\\"1") - singleQuote String @default('s\\'1') - json Json @default("{\\"theme\\": \\"light\\", \\"consoleDrawer\\": false}") -} - `, - { provider: 'postgresql', dbUrl: 'env("DATABASE_URL")', pushDb: false } - ); - }); -}); diff --git a/tests/regression/tests/issue-646.test.ts b/tests/regression/tests/issue-646.test.ts deleted file mode 100644 index ff0f8859e..000000000 --- a/tests/regression/tests/issue-646.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 646', () => { - it('regression', async () => { - await loadSchema(` -model Example { - id Int @id - epsilon Decimal @default(0.00000001) -} - `); - }); -}); diff --git a/tests/regression/tests/issue-657.test.ts b/tests/regression/tests/issue-657.test.ts deleted file mode 100644 index 1d35894f1..000000000 --- a/tests/regression/tests/issue-657.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; -import Decimal from 'decimal.js'; - -describe('Regression: issue 657', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema(` -model Foo { - id Int @id @default(autoincrement()) - intNumber Int @gt(0) - floatNumber Float @gt(0) - decimalNumber Decimal @gt(0.1) @lte(10) -} - `); - - const schema = zodSchemas.models.FooUpdateSchema; - expect(schema.safeParse({ intNumber: 0 }).success).toBeFalsy(); - expect(schema.safeParse({ intNumber: 1 }).success).toBeTruthy(); - expect(schema.safeParse({ floatNumber: 0 }).success).toBeFalsy(); - expect(schema.safeParse({ floatNumber: 1.1 }).success).toBeTruthy(); - expect(schema.safeParse({ decimalNumber: 0 }).success).toBeFalsy(); - expect(schema.safeParse({ decimalNumber: '0' }).success).toBeFalsy(); - expect(schema.safeParse({ decimalNumber: new Decimal(0) }).success).toBeFalsy(); - expect(schema.safeParse({ decimalNumber: 11 }).success).toBeFalsy(); - expect(schema.safeParse({ decimalNumber: '11.123456789' }).success).toBeFalsy(); - expect(schema.safeParse({ decimalNumber: new Decimal('11.123456789') }).success).toBeFalsy(); - expect(schema.safeParse({ decimalNumber: 10 }).success).toBeTruthy(); - expect(schema.safeParse({ decimalNumber: '10' }).success).toBeTruthy(); - expect(schema.safeParse({ decimalNumber: new Decimal('10') }).success).toBeTruthy(); - }); -}); diff --git a/tests/regression/tests/issue-665.test.ts b/tests/regression/tests/issue-665.test.ts deleted file mode 100644 index b6552fd2b..000000000 --- a/tests/regression/tests/issue-665.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 665', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - admin Boolean @default(false) - username String @unique @allow("all", auth() == this) @allow("all", auth().admin) - password String @password @default("") @allow("all", auth() == this) @allow("all", auth().admin) - firstName String @default("") - lastName String @default("") - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ data: { id: 1, username: 'test', password: 'test', admin: true } }); - - // admin - let r = await enhance({ id: 1, admin: true }).user.findFirst(); - expect(r.username).toEqual('test'); - - // owner - r = await enhance({ id: 1 }).user.findFirst(); - expect(r.username).toEqual('test'); - - // anonymous - r = await enhance().user.findFirst(); - expect(r.username).toBeUndefined(); - - // non-owner - r = await enhance({ id: 2 }).user.findFirst(); - expect(r.username).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-674.test.ts b/tests/regression/tests/issue-674.test.ts deleted file mode 100644 index db8be80d7..000000000 --- a/tests/regression/tests/issue-674.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 674', () => { - it('regression', async () => { - await loadSchema( - ` -model Foo { - id Int @id -} - -enum MyUnUsedEnum { ABC CDE @@map('my_unused_enum') } - `, - { provider: 'postgresql', dbUrl: 'env("DATABASE_URL")', pushDb: false } - ); - }); -}); diff --git a/tests/regression/tests/issue-689.test.ts b/tests/regression/tests/issue-689.test.ts deleted file mode 100644 index 32687abca..000000000 --- a/tests/regression/tests/issue-689.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 689', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model UserRole { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - role String - - @@allow('all', true) - } - - model User { - id Int @id @default(autoincrement()) - userRole UserRole[] - deleted Boolean @default(false) - - @@allow('create,read', true) - @@allow('all', auth() == this) - @@allow('all', userRole?[user == auth() && 'Admin' == role]) - @@allow('read', userRole?[user == auth()]) - } - ` - ); - - await prisma.user.create({ - data: { - id: 1, - userRole: { - create: [ - { id: 1, role: 'Admin' }, - { id: 2, role: 'Student' }, - ], - }, - }, - }); - - await prisma.user.create({ - data: { - id: 2, - userRole: { - connect: { id: 1 }, - }, - }, - }); - - const c1 = await prisma.user.count({ - where: { - userRole: { - some: { role: 'Student' }, - }, - NOT: { deleted: true }, - }, - }); - - const db = enhance(); - const c2 = await db.user.count({ - where: { - userRole: { - some: { role: 'Student' }, - }, - NOT: { deleted: true }, - }, - }); - - expect(c1).toEqual(c2); - }); -}); diff --git a/tests/regression/tests/issue-703.test.ts b/tests/regression/tests/issue-703.test.ts deleted file mode 100644 index 203100496..000000000 --- a/tests/regression/tests/issue-703.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 703', () => { - it('regression', async () => { - await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - name String? - admin Boolean @default(false) - - companiesWorkedFor Company[] - - username String @unique @allow("all", auth() == this) @allow('read', companiesWorkedFor?[owner == auth()]) @allow("all", auth().admin) - } - - model Company { - id Int @id @default(autoincrement()) - name String? - owner User @relation(fields: [ownerId], references: [id]) - ownerId Int - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-714.test.ts b/tests/regression/tests/issue-714.test.ts deleted file mode 100644 index 673d3a689..000000000 --- a/tests/regression/tests/issue-714.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; - -const DB_NAME = 'issue-714'; - -describe('Regression: issue 714', () => { - let dbUrl: string; - let prisma: any; - - beforeEach(async () => { - dbUrl = await createPostgresDb(DB_NAME); - }); - - afterEach(async () => { - if (prisma) { - await prisma.$disconnect(); - } - await dropPostgresDb(DB_NAME); - }); - - it('regression', async () => { - const { prisma: _prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - username String @unique - - employedBy CompanyUser[] - properties PropertyUser[] - companies Company[] - - @@allow('all', true) - } - - model Company { - id Int @id @default(autoincrement()) - name String - - companyUsers CompanyUser[] - propertyUsers User[] - properties Property[] - - @@allow('all', true) - } - - model CompanyUser { - company Company @relation(fields: [companyId], references: [id]) - companyId Int - user User @relation(fields: [userId], references: [id]) - userId Int - - dummyField String - - @@id([companyId, userId]) - - @@allow('all', true) - } - - enum PropertyUserRoleType { - Owner - Administrator - } - - model PropertyUserRole { - id Int @id @default(autoincrement()) - type PropertyUserRoleType - - user PropertyUser @relation(fields: [userId], references: [id]) - userId Int - - @@allow('all', true) - } - - model PropertyUser { - id Int @id @default(autoincrement()) - dummyField String - - property Property @relation(fields: [propertyId], references: [id]) - propertyId Int - user User @relation(fields: [userId], references: [id]) - userId Int - - roles PropertyUserRole[] - - @@unique([propertyId, userId]) - - @@allow('all', true) - } - - model Property { - id Int @id @default(autoincrement()) - name String - - users PropertyUser[] - company Company @relation(fields: [companyId], references: [id]) - companyId Int - - @@allow('all', true) - } - `, - { - provider: 'postgresql', - dbUrl, - } - ); - - prisma = _prisma; - const db = enhance(); - - await db.user.create({ - data: { - username: 'test@example.com', - }, - }); - - await db.company.create({ - data: { - name: 'My Company', - companyUsers: { - create: { - dummyField: '', - user: { - connect: { - id: 1, - }, - }, - }, - }, - propertyUsers: { - connect: { - id: 1, - }, - }, - properties: { - create: [ - { - name: 'Test', - }, - ], - }, - }, - }); - - await db.property.update({ - data: { - users: { - create: { - dummyField: '', - roles: { - createMany: { - data: { - type: 'Owner', - }, - }, - }, - user: { - connect: { - id: 1, - }, - }, - }, - }, - }, - where: { - id: 1, - }, - }); - }); -}); diff --git a/tests/regression/tests/issue-724.test.ts b/tests/regression/tests/issue-724.test.ts deleted file mode 100644 index d660c8281..000000000 --- a/tests/regression/tests/issue-724.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 724', () => { - it('regression', async () => { - await loadSchema( - ` - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "sqlserver" - url = env("DATABASE_URL") - } - - plugin trpc { - provider = '@zenstackhq/trpc' - output = 'src/server/routers/generated' - } - - model LastLocation { - LastLocationID String @id(map: "PK_LastLocation") @db.UniqueIdentifier - UserID String @db.UniqueIdentifier - JobID String? @db.UniqueIdentifier - Timestamp DateTime? @db.DateTime - Latitude String? @db.VarChar(Max) - Longitude String? @db.NVarChar(Max) - MostRecentTimestamp DateTime? @db.DateTime - CreatedDate DateTime @default(now(), map: "DF_Address_CreatedDate") @db.DateTime - - @@index([UserID], map: "IX_UserID") - } - `, - { addPrelude: false, pushDb: false } - ); - }); -}); diff --git a/tests/regression/tests/issue-735.test.ts b/tests/regression/tests/issue-735.test.ts deleted file mode 100644 index ddfd251c5..000000000 --- a/tests/regression/tests/issue-735.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 735', () => { - it('regression', async () => { - await loadSchema( - ` - model MyModel { - id String @id @default(cuid()) - view String - import Int - } - - model view { - id String @id @default(cuid()) - name String - } - `, - { pushDb: false } - ); - }); -}); diff --git a/tests/regression/tests/issue-744.test.ts b/tests/regression/tests/issue-744.test.ts deleted file mode 100644 index d46d110ec..000000000 --- a/tests/regression/tests/issue-744.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getObjectLiteral } from '@zenstackhq/sdk'; -import { Plugin, PluginField, isPlugin } from '@zenstackhq/sdk/ast'; -import { loadModel } from '@zenstackhq/testtools'; - -describe('Regression: issue 744', () => { - it('regression', async () => { - const model = await loadModel( - ` - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - } - - plugin zod { - provider = '@core/zod' - settings = { - '200': { status: 'ok' }, - 'x-y-z': 200, - foo: 'bar' - } - } - - model Foo { - id String @id @default(cuid()) - } - ` - ); - - const plugin = model.declarations.find((d): d is Plugin => isPlugin(d)); - const settings = plugin?.fields.find((f): f is PluginField => f.name === 'settings'); - const value: any = getObjectLiteral(settings?.value); - expect(value['200']).toMatchObject({ status: 'ok' }); - expect(value['x-y-z']).toBe(200); - expect(value.foo).toBe('bar'); - }); -}); diff --git a/tests/regression/tests/issue-756.test.ts b/tests/regression/tests/issue-756.test.ts deleted file mode 100644 index dd1a10ccf..000000000 --- a/tests/regression/tests/issue-756.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { loadModel, loadModelWithError } from '@zenstackhq/testtools'; - -describe('Regression: issue 756', () => { - it('regression', async () => { - expect( - await loadModelWithError( - ` - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - } - - model User { - id Int @id @default(autoincrement()) - email Int - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - author User? @relation(fields: [authorId], references: [id]) - authorId Int - @@allow('all', auth().posts.authorId == authorId) - } - ` - ) - ).toContain(`Could not resolve reference to MemberAccessTarget named 'authorId'.`); - }); -}); diff --git a/tests/regression/tests/issue-764.test.ts b/tests/regression/tests/issue-764.test.ts deleted file mode 100644 index 8e64a15d8..000000000 --- a/tests/regression/tests/issue-764.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 764', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - name String - - post Post? @relation(fields: [postId], references: [id]) - postId Int? - - @@allow('all', true) - } - - model Post { - id Int @id @default(autoincrement()) - title String - User User[] - - @@allow('all', true) - } - ` - ); - - const db = enhance(); - - const user = await prisma.user.create({ - data: { name: 'Me' }, - }); - - await db.user.update({ - where: { id: user.id }, - data: { - post: { - upsert: { - create: { - title: 'Hello World', - }, - update: { - title: 'Hello World', - }, - }, - }, - }, - }); - }); -}); diff --git a/tests/regression/tests/issue-765.test.ts b/tests/regression/tests/issue-765.test.ts deleted file mode 100644 index 13f88bf19..000000000 --- a/tests/regression/tests/issue-765.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 765', () => { - it('regression', async () => { - const { enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - name String - - post Post? @relation(fields: [postId], references: [id]) - postId Int? - - @@allow('all', true) - } - - model Post { - id Int @id @default(autoincrement()) - title String - User User[] - - @@allow('all', true) - } - `, - { - previewFeatures: [], - } - ); - - const db = enhance(); - const r = await db.user.create({ - data: { - name: 'Me', - post: undefined, - }, - }); - expect(r.name).toBe('Me'); - expect(r.post).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-804.test.ts b/tests/regression/tests/issue-804.test.ts deleted file mode 100644 index 18659b1ec..000000000 --- a/tests/regression/tests/issue-804.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { loadModelWithError } from '@zenstackhq/testtools'; - -describe('Regression: issue 804', () => { - it('regression', async () => { - expect( - await loadModelWithError( - ` - generator client { - provider = "prisma-client-js" - } - - datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - } - - model User { - id Int @id @default(autoincrement()) - email Int - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - author User? @relation(fields: [authorId], references: [id]) - authorId Int - published Boolean - - @@allow('all', auth().posts?[published] == 'TRUE') - } - ` - ) - ).toContain('incompatible operand types'); - }); -}); diff --git a/tests/regression/tests/issue-811.test.ts b/tests/regression/tests/issue-811.test.ts deleted file mode 100644 index af2268203..000000000 --- a/tests/regression/tests/issue-811.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 811', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model Membership { - id String @id @default(uuid()) - role String @default('STANDARD') - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String @unique - - @@auth - @@allow('create,update,delete', auth().role == 'ADMIN') - @@allow('update', auth() == this) - @@allow('read', true) - } - model User { - id String @id @default(uuid()) - profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) - profileId String @unique - memberships Membership[] - - @@allow('create,update,delete', auth().role == 'ADMIN') - @@allow('update', id == auth().userId) - @@allow('read', true) - } - model Profile { - id String @id @default(uuid()) - firstName String - users User[] - - @@allow('create,update,delete', auth().role == 'ADMIN') - @@allow('update', users?[id == auth().userId]) - @@allow('read', true) - } - ` - ); - - const r = await prisma.user.create({ - data: { - profile: { - create: { firstName: 'Tom' }, - }, - memberships: { - create: { role: 'STANDARD' }, - }, - }, - include: { - profile: true, - memberships: true, - }, - }); - - const membershipId = r.memberships[0].id; - const userId = r.id; - const db = enhance({ id: membershipId, role: 'ADMIN', userId }); - - const r1 = await db.membership.update({ - data: { - role: 'VIP', - user: { update: { data: { profile: { update: { data: { firstName: 'Jerry' } } } } } }, - }, - include: { user: { include: { profile: true } } }, - where: { id: membershipId }, - }); - - expect(r1.role).toBe('VIP'); - expect(r1.user.profile.firstName).toBe('Jerry'); - }); -}); diff --git a/tests/regression/tests/issue-814.test.ts b/tests/regression/tests/issue-814.test.ts deleted file mode 100644 index ef38a80fc..000000000 --- a/tests/regression/tests/issue-814.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 814', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - profile Profile? - - @@allow('all', true) - } - - model Profile { - id Int @id @default(autoincrement()) - name String @allow('read', !private) - private Boolean @default(false) - user User @relation(fields: [userId], references: [id]) - userId Int @unique - - @@allow('all', true) - } - ` - ); - - const user = await prisma.user.create({ - data: { profile: { create: { name: 'Foo', private: true } } }, - include: { profile: true }, - }); - - const r = await enhance().profile.findFirst({ where: { id: user.profile.id } }); - expect(r.name).toBeUndefined(); - - const r1 = await enhance().user.findFirst({ - where: { id: user.id }, - include: { profile: true }, - }); - expect(r1.profile.name).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-825.test.ts b/tests/regression/tests/issue-825.test.ts deleted file mode 100644 index d45d5f782..000000000 --- a/tests/regression/tests/issue-825.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 825', () => { - it('regression', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - role String - - @@allow('read', true) - @@allow('update', auth().id == id || auth().role == 'superadmin' || auth().role == 'admin') - @@deny('update', - (role == 'superadmin' && auth().id != id) - || (role == 'admin' && auth().id != id && auth().role != 'superadmin') - || (role != future().role && auth().role != 'admin' && auth().role != 'superadmin') - || (role != future().role && future().role == 'superadmin') - || (role != future().role && future().role == 'admin' && auth().role != 'superadmin') - ) - } - ` - ); - - const admin = await prisma.user.create({ - data: { role: 'admin' }, - }); - - const user = await prisma.user.create({ - data: { role: 'customer' }, - }); - - const r = await enhance(admin).user.update({ - where: { id: user.id }, - data: { role: 'staff' }, - }); - - expect(r.role).toEqual('staff'); - }); -}); diff --git a/tests/regression/tests/issue-864.test.ts b/tests/regression/tests/issue-864.test.ts deleted file mode 100644 index eb50bc95f..000000000 --- a/tests/regression/tests/issue-864.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 864', () => { - it('safe create', async () => { - const { prisma, enhance } = await loadSchema( - ` - model A { - id Int @id @default(autoincrement()) - aValue Int - b B[] - - @@allow('all', aValue > 0) - } - - model B { - id Int @id @default(autoincrement()) - bValue Int - aId Int - a A @relation(fields: [aId], references: [id]) - c C[] - - @@allow('all', bValue > 0) - } - - model C { - id Int @id @default(autoincrement()) - cValue Int - bId Int - b B @relation(fields: [bId], references: [id]) - - @@allow('all', cValue > 0) - } - ` - ); - - await prisma.a.create({ - data: { id: 1, aValue: 1, b: { create: { id: 2, bValue: 2 } } }, - include: { b: true }, - }); - - const db = enhance(); - await db.a.update({ - where: { id: 1 }, - data: { - b: { - update: [ - { - where: { id: 2 }, - data: { - c: { - create: [ - { - cValue: 3, - }, - ], - }, - }, - }, - ], - }, - }, - }); - }); - - it('unsafe create nested in to-many', async () => { - const { prisma, enhance } = await loadSchema( - ` - model A { - id Int @id @default(autoincrement()) - aValue Int - b B[] - - @@allow('all', aValue > 0) - } - - model B { - id Int @id @default(autoincrement()) - bValue Int - aId Int - a A @relation(fields: [aId], references: [id]) - c C[] - - @@allow('all', bValue > 0) - } - - model C { - id Int @id @default(autoincrement()) - cValue Int - bId Int - b B @relation(fields: [bId], references: [id]) - - @@allow('all', cValue > 0) - } - ` - ); - - await prisma.a.create({ - data: { id: 1, aValue: 1, b: { create: { id: 2, bValue: 2 } } }, - include: { b: true }, - }); - - const db = enhance(); - await db.a.update({ - where: { id: 1 }, - data: { - b: { - update: [ - { - where: { id: 2 }, - data: { - c: { - create: [ - { - id: 1, - cValue: 3, - }, - ], - }, - }, - }, - ], - }, - }, - }); - }); - - it('unsafe create nested in to-one', async () => { - const { prisma, enhance } = await loadSchema( - ` - model A { - id Int @id @default(autoincrement()) - aValue Int - b B? - - @@allow('all', aValue > 0) - } - - model B { - id Int @id @default(autoincrement()) - bValue Int - aId Int @unique - a A @relation(fields: [aId], references: [id]) - c C[] - - @@allow('all', bValue > 0) - } - - model C { - id Int @id @default(autoincrement()) - cValue Int - bId Int - b B @relation(fields: [bId], references: [id]) - - @@allow('all', cValue > 0) - } - ` - ); - - await prisma.a.create({ - data: { id: 1, aValue: 1, b: { create: { id: 2, bValue: 2 } } }, - include: { b: true }, - }); - - const db = enhance(); - await db.a.update({ - where: { id: 1 }, - data: { - b: { - update: { - data: { - c: { - create: [ - { - id: 1, - cValue: 3, - }, - ], - }, - }, - }, - }, - }, - }); - }); -}); diff --git a/tests/regression/tests/issue-886.test.ts b/tests/regression/tests/issue-886.test.ts deleted file mode 100644 index 4f20d9817..000000000 --- a/tests/regression/tests/issue-886.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 886', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` - model Model { - id Int @id @default(autoincrement()) - a Int @default(100) - b String @default('') - c DateTime @default(now()) - } - ` - ); - - const r = zodSchemas.models.ModelSchema.parse({ id: 1 }); - expect(r.a).toBe(100); - expect(r.b).toBe(''); - expect(r.c).toBeInstanceOf(Date); - expect(r.id).toBe(1); - }); -}); diff --git a/tests/regression/tests/issue-925.test.ts b/tests/regression/tests/issue-925.test.ts deleted file mode 100644 index b19d9d615..000000000 --- a/tests/regression/tests/issue-925.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { loadModel, loadModelWithError } from '@zenstackhq/testtools'; - -describe('Regression: issue 925', () => { - it('member reference without using this', async () => { - await expect( - loadModelWithError( - ` - model User { - id Int @id @default(autoincrement()) - company Company[] - test Int - - @@allow('read', auth().company?[staff?[companyId == test]]) - } - - model Company { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - - staff Staff[] - @@allow('read', true) - } - - model Staff { - id Int @id @default(autoincrement()) - - company Company @relation(fields: [companyId], references: [id]) - companyId Int - - @@allow('read', true) - } - ` - ) - ).resolves.toContain("Could not resolve reference to ReferenceTarget named 'test'."); - }); - - it('reference with this', async () => { - await loadModel( - ` - model User { - id Int @id @default(autoincrement()) - company Company[] - test Int - - @@allow('read', auth().company?[staff?[companyId == this.test]]) - } - - model Company { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int - - staff Staff[] - @@allow('read', true) - } - - model Staff { - id Int @id @default(autoincrement()) - - company Company @relation(fields: [companyId], references: [id]) - companyId Int - - @@allow('read', true) - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-947.test.ts b/tests/regression/tests/issue-947.test.ts deleted file mode 100644 index afabae502..000000000 --- a/tests/regression/tests/issue-947.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { loadModel } from '@zenstackhq/testtools'; - -describe('Regression: issue 947', () => { - it('regression', async () => { - await loadModel( - ` - model Test { - id String @id - props TestEnum[] @default([]) - } - - enum TestEnum { - A - B - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-961.test.ts b/tests/regression/tests/issue-961.test.ts deleted file mode 100644 index 1f622059e..000000000 --- a/tests/regression/tests/issue-961.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 961', () => { - const schema = ` - model User { - id String @id @default(cuid()) - backups UserColumnBackup[] - } - - model UserColumnBackup { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - key String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - columns UserColumn[] - @@unique([userId, key]) - @@allow('all', auth().id == userId) - } - - model UserColumn { - id String @id @default(cuid()) - userColumnBackup UserColumnBackup @relation(fields: [userColumnBackupId], references: [id], onDelete: Cascade) - userColumnBackupId String - column String - version Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt() - - @@unique([userColumnBackupId, column]) - @@allow('all', auth().id == userColumnBackup.userId) - @@deny('update,delete', column == 'c2') - } - `; - - it('deleteMany', async () => { - const { prisma, enhance } = await loadSchema(schema); - - const user = await prisma.user.create({ - data: { - backups: { - create: { - key: 'key1', - columns: { - create: [{ column: 'c1' }, { column: 'c2' }, { column: 'c3' }], - }, - }, - }, - }, - include: { backups: true }, - }); - const backup = user.backups[0]; - - const db = enhance({ id: user.id }); - - // delete with non-existing outer filter - await expect( - db.userColumnBackup.update({ - where: { id: 'abc' }, - data: { - columns: { - deleteMany: { - column: 'c1', - }, - }, - }, - }) - ).toBeNotFound(); - await expect(db.userColumn.findMany()).resolves.toHaveLength(3); - - // delete c1 - await db.userColumnBackup.update({ - where: { id: backup.id }, - data: { - columns: { - deleteMany: { - column: 'c1', - }, - }, - }, - include: { columns: true }, - }); - await expect(db.userColumn.findMany()).resolves.toHaveLength(2); - - // delete c1 again, no change - await db.userColumnBackup.update({ - where: { id: backup.id }, - data: { - columns: { - deleteMany: { - column: 'c1', - }, - }, - }, - }); - await expect(db.userColumn.findMany()).resolves.toHaveLength(2); - - // delete c2, filtered out by policy - await db.userColumnBackup.update({ - where: { id: backup.id }, - data: { - columns: { - deleteMany: { - column: 'c2', - }, - }, - }, - }); - await expect(db.userColumn.findMany()).resolves.toHaveLength(2); - - // delete c3, should succeed - await db.userColumnBackup.update({ - where: { id: backup.id }, - data: { - columns: { - deleteMany: { - column: 'c3', - }, - }, - }, - }); - await expect(db.userColumn.findMany()).resolves.toHaveLength(1); - }); - - it('updateMany', async () => { - const { prisma, enhance } = await loadSchema(schema); - - const user = await prisma.user.create({ - data: { - backups: { - create: { - key: 'key1', - columns: { - create: [ - { column: 'c1', version: 1 }, - { column: 'c2', version: 2 }, - ], - }, - }, - }, - }, - include: { backups: true }, - }); - const backup = user.backups[0]; - - const db = enhance({ id: user.id }); - - // update with non-existing outer filter - await expect( - db.userColumnBackup.update({ - where: { id: 'abc' }, - data: { - columns: { - updateMany: { - where: { column: 'c1' }, - data: { version: { increment: 1 } }, - }, - }, - }, - }) - ).toBeNotFound(); - await expect(db.userColumn.findMany()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ column: 'c1', version: 1 }), - expect.objectContaining({ column: 'c2', version: 2 }), - ]) - ); - - // update c1 - await db.userColumnBackup.update({ - where: { id: backup.id }, - data: { - columns: { - updateMany: { - where: { column: 'c1' }, - data: { version: { increment: 1 } }, - }, - }, - }, - include: { columns: true }, - }); - await expect(db.userColumn.findMany()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ column: 'c1', version: 2 }), - expect.objectContaining({ column: 'c2', version: 2 }), - ]) - ); - - // update c2, filtered out by policy - await db.userColumnBackup.update({ - where: { id: backup.id }, - data: { - columns: { - updateMany: { - where: { column: 'c2' }, - data: { version: { increment: 1 } }, - }, - }, - }, - include: { columns: true }, - }); - await expect(db.userColumn.findMany()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ column: 'c1', version: 2 }), - expect.objectContaining({ column: 'c2', version: 2 }), - ]) - ); - }); -}); diff --git a/tests/regression/tests/issue-965.test.ts b/tests/regression/tests/issue-965.test.ts deleted file mode 100644 index 79bd92075..000000000 --- a/tests/regression/tests/issue-965.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { loadModel, loadModelWithError } from '@zenstackhq/testtools'; - -describe('Regression: issue 965', () => { - it('regression1', async () => { - await loadModel(` - abstract model Base { - id String @id @default(cuid()) - } - - abstract model A { - URL String? @url - } - - abstract model B { - anotherURL String? @url - } - - abstract model C { - oneMoreURL String? @url - } - - model D extends Base, A, B { - } - - model E extends Base, B, C { - }`); - }); - - it('regression2', async () => { - await expect( - loadModelWithError(` - abstract model A { - URL String? @url - } - - abstract model B { - anotherURL String? @url - } - - abstract model C { - oneMoreURL String? @url - } - - model D extends A, B { - } - - model E extends B, C { - }`) - ).resolves.toContain( - '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.' - ); - }); -}); diff --git a/tests/regression/tests/issue-971.test.ts b/tests/regression/tests/issue-971.test.ts deleted file mode 100644 index 40990aa6a..000000000 --- a/tests/regression/tests/issue-971.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 971', () => { - it('regression', async () => { - await loadSchema( - ` - abstract model Level1 { - id String @id @default(cuid()) - URL String? - @@validate(URL != null, "URL must be provided") // works - } - abstract model Level2 extends Level1 { - @@validate(URL != null, "URL must be provided") // works - } - abstract model Level3 extends Level2 { - @@validate(URL != null, "URL must be provided") // doesn't work - } - model Foo extends Level3 { - } - ` - ); - }); -}); diff --git a/tests/regression/tests/issue-992.test.ts b/tests/regression/tests/issue-992.test.ts deleted file mode 100644 index 40a1aac47..000000000 --- a/tests/regression/tests/issue-992.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('Regression: issue 992', () => { - it('regression', async () => { - const { enhance, prisma } = await loadSchema( - ` - model Product { - id String @id @default(cuid()) - category Category @relation(fields: [categoryId], references: [id]) - categoryId String - - deleted Int @default(0) @omit - @@deny('read', deleted != 0) - @@allow('all', true) - } - - model Category { - id String @id @default(cuid()) - products Product[] - @@allow('all', true) - } - ` - ); - - await prisma.category.create({ - data: { - products: { - create: [ - { - deleted: 0, - }, - { - deleted: 0, - }, - ], - }, - }, - }); - - const db = enhance(); - const category = await db.category.findFirst({ include: { products: true } }); - expect(category.products[0].deleted).toBeUndefined(); - expect(category.products[1].deleted).toBeUndefined(); - }); -}); diff --git a/tests/regression/tests/issue-foo.test.ts b/tests/regression/tests/issue-foo.test.ts deleted file mode 100644 index 19cf246ef..000000000 --- a/tests/regression/tests/issue-foo.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue [...]', () => { - it('regression', async () => { - const { zodSchemas } = await loadSchema( - ` - enum FooType { - Bar - Baz - } - type Meta { - test String? - } - model Foo { - id String @id @db.Uuid @default(uuid()) - type FooType - meta Meta @json - @@validate(type == Bar, "FooType must be Bar") - } - `, - { - provider: 'postgresql', - pushDb: false, - } - ); - expect( - zodSchemas.models.FooSchema.safeParse({ - id: '123e4567-e89b-12d3-a456-426614174000', - type: 'Bar', - meta: { test: 'test' }, - }) - ).toMatchObject({ - success: true, - }); - }); -}); diff --git a/tests/regression/tests/issue-prisma-extension.test.ts b/tests/regression/tests/issue-prisma-extension.test.ts deleted file mode 100644 index fa041a18a..000000000 --- a/tests/regression/tests/issue-prisma-extension.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { loadSchema } from '@zenstackhq/testtools'; - -describe('issue prisma extension', () => { - it('extend enhanced client', async () => { - const { enhance, prisma } = await loadSchema( - ` - model Post { - id Int @id - title String - published Boolean - - @@allow('create', true) - @@allow('read', published) - } - ` - ); - - await prisma.post.create({ data: { id: 1, title: 'post1', published: true } }); - await prisma.post.create({ data: { id: 2, title: 'post2', published: false } }); - - const db = enhance(); - await expect(db.post.findMany()).resolves.toHaveLength(1); - - const extended = db.$extends({ - model: { - post: { - findManyListView: async (args: any) => { - return { view: true, data: await db.post.findMany(args) }; - }, - }, - }, - }); - - await expect(extended.post.findManyListView()).resolves.toMatchObject({ - view: true, - data: [{ id: 1, title: 'post1', published: true }], - }); - await expect(extended.post.findMany()).resolves.toHaveLength(1); - }); - - it('enhance extended client', async () => { - const { enhanceRaw, prisma, prismaModule } = await loadSchema( - ` - model Post { - id Int @id - title String - published Boolean - - @@allow('create', true) - @@allow('read', published) - } - ` - ); - - await prisma.post.create({ data: { id: 1, title: 'post1', published: true } }); - await prisma.post.create({ data: { id: 2, title: 'post2', published: false } }); - - const ext = prismaModule.defineExtension((_prisma: any) => { - return _prisma.$extends({ - model: { - post: { - findManyListView: async (args: any) => { - return { view: true, data: await prisma.post.findMany(args) }; - }, - }, - }, - }); - }); - - await expect(prisma.$extends(ext).post.findMany()).resolves.toHaveLength(2); - await expect(prisma.$extends(ext).post.findManyListView()).resolves.toMatchObject({ - view: true, - data: [ - { id: 1, title: 'post1', published: true }, - { id: 2, title: 'post2', published: false }, - ], - }); - - const enhanced = enhanceRaw(prisma.$extends(ext)); - await expect(enhanced.post.findMany()).resolves.toHaveLength(1); - // findManyListView internally uses the un-enhanced client - await expect(enhanced.post.findManyListView()).resolves.toMatchObject({ - view: true, - data: [ - { id: 1, title: 'post1', published: true }, - { id: 2, title: 'post2', published: false }, - ], - }); - }); -}); diff --git a/tests/regression/tests/issues.test.ts b/tests/regression/tests/issues.test.ts deleted file mode 100644 index 1418a309a..000000000 --- a/tests/regression/tests/issues.test.ts +++ /dev/null @@ -1,792 +0,0 @@ -import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; -import path from 'path'; - -describe('GitHub issues regression', () => { - let origDir: string; - - beforeAll(async () => { - origDir = path.resolve('.'); - }); - - afterEach(() => { - process.chdir(origDir); - }); - - it('issue 389', async () => { - const { enhance } = await loadSchema(` - model model { - id String @id @default(uuid()) - value Int - @@allow('read', true) - @@allow('create', value > 0) - } - `); - const db = enhance(); - await expect(db.model.create({ data: { value: 0 } })).toBeRejectedByPolicy(); - await expect(db.model.create({ data: { value: 1 } })).toResolveTruthy(); - }); - - it('issue 392', async () => { - await loadSchema( - ` - model M1 { - m2_id String @id - m2 M2 @relation(fields: [m2_id], references: [id]) - } - - model M2 { - id String @id - m1 M1? - } - ` - ); - - await loadSchema( - ` - model M1 { - id String @id - m2_id String @unique - m2 M2 @relation(fields: [m2_id], references: [id]) - } - - model M2 { - id String @id - m1 M1? - } - ` - ); - - await loadSchema( - ` - model M1 { - m2_id String - m2 M2 @relation(fields: [m2_id], references: [id]) - @@id([m2_id]) - } - - model M2 { - id String @id - m1 M1? - } - ` - ); - - await loadSchema( - ` - model M1 { - m2_id String - m2 M2 @relation(fields: [m2_id], references: [id]) - @@unique([m2_id]) - } - - model M2 { - id String @id - m1 M1? - } - ` - ); - }); - - it('select with _count', async () => { - const { prisma, enhance } = await loadSchema( - ` - model User { - id String @id @unique @default(uuid()) - posts Post[] - - @@allow('all', true) - } - - model Post { - id String @id @default(uuid()) - title String - published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String - - @@allow('all', true) - } - ` - ); - - await prisma.user.create({ - data: { - posts: { - create: [{ title: 'Post 1' }, { title: 'Post 2' }], - }, - }, - }); - - const db = enhance(); - const r = await db.user.findFirst({ select: { _count: { select: { posts: true } } } }); - expect(r).toMatchObject({ _count: { posts: 2 } }); - }); - - it('issue 509', async () => { - await loadSchema( - ` - model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - posts Post[] - } - - model Post { - id Int @id @default(autoincrement()) - title String - content String? - published Boolean @default(false) - author User? @relation(fields: [authorId], references: [id]) - authorId Int? - - deleted Boolean @default(false) @omit - - @@allow('all', true) - @@deny('read', deleted) - } - ` - ); - }); - - it('issue 552', async () => { - const { enhance, prisma } = await loadSchema( - ` - model Tenant { - id Int @id @default(autoincrement()) - name String - - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - - users UserTenant[] - - @@map("tenants") - - - @@allow('all', auth().is_super_admin == true) - @@allow('read', users?[user == auth() && status == 'ACTIVE' ]) - @@allow('all', users?[user == auth() && status == 'ACTIVE']) - } - - model User { - id Int @id @default(autoincrement()) - name String - is_super_admin Boolean @default(false) @omit - - created_at DateTime @default(now()) - updated_at DateTime @updatedAt - - associated_tenants UserTenant[] - - @@map("users") - - @@allow('read', auth().id == id) - @@allow('all', auth().is_super_admin == true ) - @@allow('read', associated_tenants?[tenant.users?[user == auth() && status == 'ACTIVE']]) - @@allow('all', associated_tenants?[tenant.users?[user == auth() && status == 'ACTIVE']] ) - @@allow('create', associated_tenants?[tenant.users?[user == auth() && status == 'ACTIVE']] ) - @@allow('update', associated_tenants?[tenant.users?[user == auth() && status == 'ACTIVE']] ) - } - - model UserTenant { - user_id Int - user User @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade) - - tenant_id Int - tenant Tenant @relation(fields: [tenant_id], references: [id], onDelete: Cascade, onUpdate: Cascade) - - status String @default('INACTIVE') - - @@map("user_tenants") - - @@id([user_id, tenant_id]) - - @@index([user_id]) - @@index([tenant_id]) - @@index([user_id, tenant_id]) - - @@allow('all', auth().is_super_admin == true ) - @@allow('read', tenant.users?[user == auth() && status == 'ACTIVE' ]) - @@allow('all', tenant.users?[user == auth() && status == 'ACTIVE']) - @@allow('update', tenant.users?[user == auth() && status == 'ACTIVE']) - @@allow('delete', tenant.users?[user == auth() && status == 'ACTIVE']) - @@allow('create', tenant.users?[user == auth() && status == 'ACTIVE']) - } - ` - ); - - await prisma.user.deleteMany(); - await prisma.tenant.deleteMany(); - - await prisma.tenant.create({ - data: { - id: 1, - name: 'tenant 1', - }, - }); - - await prisma.user.create({ - data: { - id: 1, - name: 'user 1', - }, - }); - - await prisma.userTenant.create({ - data: { - user_id: 1, - tenant_id: 1, - }, - }); - - const db = enhance({ id: 1, is_super_admin: true }); - await db.userTenant.update({ - where: { - user_id_tenant_id: { - user_id: 1, - tenant_id: 1, - }, - }, - data: { - user: { - update: { - name: 'user 1 updated', - }, - }, - }, - }); - }); - - it('issue 609', async () => { - const { enhance, prisma } = await loadSchema( - ` - model User { - id String @id @default(cuid()) - comments Comment[] - } - - model Comment { - id String @id @default(cuid()) - parentCommentId String? - replies Comment[] @relation("CommentToComment") - parent Comment? @relation("CommentToComment", fields: [parentCommentId], references: [id]) - comment String - author User @relation(fields: [authorId], references: [id]) - authorId String - - @@allow('read,create', true) - @@allow('update,delete', auth() == author) - } - ` - ); - - await prisma.user.create({ - data: { - id: '1', - comments: { - create: { - id: '1', - comment: 'Comment 1', - }, - }, - }, - }); - - await prisma.user.create({ - data: { - id: '2', - }, - }); - - // connecting a child comment from a different user to a parent comment should succeed - const db = enhance({ id: '2' }); - await expect( - db.comment.create({ - data: { - comment: 'Comment 2', - author: { connect: { id: '2' } }, - parent: { connect: { id: '1' } }, - }, - }) - ).toResolveTruthy(); - }); - - it('issue 624', async () => { - const { prisma, enhance } = await loadSchema( - ` -model User { - id String @id @default(uuid()) - email String @unique - password String? @password @omit - name String? - orgs Organization[] - posts Post[] - groups Group[] - comments Comment[] - // can be created by anyone, even not logged in - @@allow('create', true) - // can be read by users in the same organization - @@allow('read', orgs?[members?[auth().id == id]]) - // full access by oneself - @@allow('all', auth().id == id) -} - -model Organization { - id String @id @default(uuid()) - name String - members User[] - post Post[] - groups Group[] - comments Comment[] - - // everyone can create a organization - @@allow('create', true) - // any user in the organization can read the organization - @@allow('read', members?[auth().id == id]) -} - -abstract model organizationBaseEntity { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - isDeleted Boolean @default(false) @omit - isPublic Boolean @default(false) - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) - orgId String - groups Group[] - - // when create, owner must be set to current user, and user must be in the organization - @@allow('create', owner == auth() && org.members?[id == auth().id]) - // only the owner can update it and is not allowed to change the owner - @@allow('update', owner == auth() && org.members?[id == auth().id] && future().owner == owner) - // allow owner to read - @@allow('read', owner == auth()) - // allow shared group members to read it - @@allow('read', groups?[users?[id == auth().id]]) - // allow organization to access if public - @@allow('read', isPublic && org.members?[id == auth().id]) - // can not be read if deleted - @@deny('all', isDeleted == true) -} - -model Post extends organizationBaseEntity { - title String - content String - comments Comment[] -} - -model Comment extends organizationBaseEntity { - content String - post Post @relation(fields: [postId], references: [id]) - postId String -} - -model Group { - id String @id @default(uuid()) - name String - users User[] - posts Post[] - comments Comment[] - org Organization @relation(fields: [orgId], references: [id]) - orgId String - - // group is shared by organization - @@allow('all', org.members?[auth().id == id]) -} - ` - ); - - const userData = [ - { - id: 'robin@prisma.io', - name: 'Robin', - email: 'robin@prisma.io', - orgs: { - create: [ - { - id: 'prisma', - name: 'prisma', - }, - ], - }, - groups: { - create: [ - { - id: 'community', - name: 'community', - orgId: 'prisma', - }, - ], - }, - posts: { - create: [ - { - id: 'slack', - title: 'Join the Prisma Slack', - content: 'https://slack.prisma.io', - orgId: 'prisma', - comments: { - create: [ - { - id: 'comment-1', - content: 'This is the first comment', - orgId: 'prisma', - ownerId: 'robin@prisma.io', - }, - ], - }, - }, - ], - }, - }, - { - id: 'bryan@prisma.io', - name: 'Bryan', - email: 'bryan@prisma.io', - orgs: { - connect: { - id: 'prisma', - }, - }, - posts: { - create: [ - { - id: 'discord', - title: 'Join the Prisma Discord', - content: 'https://discord.gg/jS3XY7vp46', - orgId: 'prisma', - groups: { - connect: { - id: 'community', - }, - }, - }, - ], - }, - }, - ]; - - for (const u of userData) { - const user = await prisma.user.create({ - data: u, - }); - console.log(`Created user with id: ${user.id}`); - } - - const db = enhance({ id: 'robin@prisma.io' }); - await expect( - db.post.findMany({ - where: {}, - select: { - id: true, - content: true, - owner: { - select: { - id: true, - name: true, - }, - }, - comments: { - select: { - id: true, - content: true, - owner: { - select: { - id: true, - name: true, - }, - }, - }, - }, - }, - }) - ).resolves.toHaveLength(2); - }); - - it('issue 627', async () => { - const { prisma, enhance } = await loadSchema( - ` -model User { - id String @id @default(uuid()) -} - -abstract model BaseEntityWithTenant { - id String @id @default(uuid()) - - name String - tenant_id String - tenant tenant? @relation(fields: [tenant_id], references: [id]) - - @@allow('all', auth().id == tenant_id) -} - -model tenant { - id String @id @default(uuid()) - equipments Equipment[] -} - -model Equipment extends BaseEntityWithTenant { - a String -} -` - ); - - await prisma.tenant.create({ - data: { - id: 'tenant-1', - }, - }); - - const db = enhance({ id: 'tenant-1' }); - await expect( - db.equipment.create({ - data: { - name: 'equipment-1', - tenant: { - connect: { - id: 'tenant-1', - }, - }, - a: 'a', - }, - }) - ).toResolveTruthy(); - }); - - it('issue 632', async () => { - const dbUrl = await createPostgresDb('issue-632'); - try { - await loadSchema( - ` -enum InventoryUnit { - DIGITAL - FL_OZ - GRAMS - MILLILITERS - OUNCES - UNIT - UNLIMITED -} - -model TwoEnumsOneModelTest { - id String @id @default(cuid()) - inventoryUnit InventoryUnit @default(UNIT) - inputUnit InventoryUnit @default(UNIT) -} -`, - { provider: 'postgresql', dbUrl } - ); - } finally { - await dropPostgresDb('issue-632'); - } - }); - - it('issue 634', async () => { - const { prisma, enhance } = await loadSchema( - ` -model User { - id String @id @default(uuid()) - email String @unique - password String? @password @omit - name String? - orgs Organization[] - posts Post[] - groups Group[] - comments Comment[] - // can be created by anyone, even not logged in - @@allow('create', true) - // can be read by users in the same organization - @@allow('read', orgs?[members?[auth().id == id]]) - // full access by oneself - @@allow('all', auth() == this) -} - -model Organization { - id String @id @default(uuid()) - name String - members User[] - post Post[] - groups Group[] - comments Comment[] - - // everyone can create a organization - @@allow('create', true) - // any user in the organization can read the organization - @@allow('read', members?[auth().id == id]) -} - -abstract model organizationBaseEntity { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - isDeleted Boolean @default(false) @omit - isPublic Boolean @default(false) - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade) - orgId String - groups Group[] - - // when create, owner must be set to current user, and user must be in the organization - @@allow('create', owner == auth() && org.members?[id == auth().id]) - // only the owner can update it and is not allowed to change the owner - @@allow('update', owner == auth() && org.members?[id == auth().id] && future().owner == owner) - // allow owner to read - @@allow('read', owner == auth()) - // allow shared group members to read it - @@allow('read', groups?[users?[id == auth().id]]) - // allow organization to access if public - @@allow('read', isPublic && org.members?[id == auth().id]) - // can not be read if deleted - @@deny('all', isDeleted == true) -} - -model Post extends organizationBaseEntity { - title String - content String - comments Comment[] -} - -model Comment extends organizationBaseEntity { - content String - post Post @relation(fields: [postId], references: [id]) - postId String -} - -model Group { - id String @id @default(uuid()) - name String - users User[] - posts Post[] - comments Comment[] - org Organization @relation(fields: [orgId], references: [id]) - orgId String - - // group is shared by organization - @@allow('all', org.members?[auth().id == id]) -} -` - ); - - const userData = [ - { - id: 'robin@prisma.io', - name: 'Robin', - email: 'robin@prisma.io', - orgs: { - create: [ - { - id: 'prisma', - name: 'prisma', - }, - ], - }, - groups: { - create: [ - { - id: 'community', - name: 'community', - orgId: 'prisma', - }, - ], - }, - posts: { - create: [ - { - id: 'slack', - title: 'Join the Prisma Slack', - content: 'https://slack.prisma.io', - orgId: 'prisma', - comments: { - create: [ - { - id: 'comment-1', - content: 'This is the first comment', - orgId: 'prisma', - ownerId: 'robin@prisma.io', - }, - ], - }, - }, - ], - }, - }, - { - id: 'bryan@prisma.io', - name: 'Bryan', - email: 'bryan@prisma.io', - orgs: { - connect: { - id: 'prisma', - }, - }, - posts: { - create: [ - { - id: 'discord', - title: 'Join the Prisma Discord', - content: 'https://discord.gg/jS3XY7vp46', - orgId: 'prisma', - groups: { - connect: { - id: 'community', - }, - }, - }, - ], - }, - }, - ]; - - for (const u of userData) { - const user = await prisma.user.create({ - data: u, - }); - console.log(`Created user with id: ${user.id}`); - } - - const db = enhance({ id: 'robin@prisma.io' }); - await expect( - db.comment.findMany({ - where: { - owner: { - name: 'Bryan', - }, - }, - select: { - id: true, - content: true, - owner: { - select: { - id: true, - name: true, - }, - }, - }, - }) - ).resolves.toHaveLength(0); - - await expect( - db.comment.findMany({ - where: { - owner: { - name: 'Robin', - }, - }, - select: { - id: true, - content: true, - owner: { - select: { - id: true, - name: true, - }, - }, - }, - }) - ).resolves.toHaveLength(1); - }); -}); diff --git a/tsconfig.base.json b/tsconfig.base.json deleted file mode 100644 index 495472c60..000000000 --- a/tsconfig.base.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES6", - "module": "commonjs", - "lib": ["ESNext"], - "sourceMap": true, - "strict": true, - "noUnusedLocals": false, - "noImplicitReturns": true, - "moduleResolution": "node", - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "resolveJsonModule": true, - "declaration": true, - "experimentalDecorators": true - } -} From 5def9479f5059c63b0050a65c2017eed851566e4 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 11 Feb 2026 12:02:34 +0800 Subject: [PATCH 8/8] chore: more repo merge cleanup (#2371) --- README.md | 4 ++-- packages/schema/tsconfig.vscode.json | 7 ------- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 packages/schema/tsconfig.vscode.json diff --git a/README.md b/README.md index 92f24ca4d..859364b29 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@
- + @@ -37,7 +37,7 @@ ZenStack is a TypeScript database toolkit for developing full-stack or backend N # What's New in V3 -ZenStack V3 is a major rewrite of [V2](https://github.com/zenstackhq/zenstack). It replaced Prisma ORM with its own ORM engine built on top of [Kysely](https://kysely.dev) while keeping a Prisma-compatible query API. This architecture change brings the level of flexibility that we couldn't imagine in previous versions. Please check [this blog post](https://zenstack.dev/blog/next-chapter-1) for why we made this bold decision. +ZenStack V3 is a major rewrite. It replaced Prisma ORM with its own ORM engine built on top of [Kysely](https://kysely.dev) while keeping a Prisma-compatible query API. This architecture change brings the level of flexibility that we couldn't imagine in previous versions. Please check [this blog post](https://zenstack.dev/blog/next-chapter-1) for why we made this bold decision. Even without using advanced features, ZenStack offers the following benefits as a drop-in replacement to Prisma: diff --git a/packages/schema/tsconfig.vscode.json b/packages/schema/tsconfig.vscode.json deleted file mode 100644 index 06ce58f3e..000000000 --- a/packages/schema/tsconfig.vscode.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src/**/*.ts"] -}