diff --git a/.coderabbit.yaml b/.coderabbit.yaml index c50c3b9e..15eddbc7 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -4,5 +4,6 @@ early_access: false reviews: auto_review: enabled: true + sequence_diagrams: false chat: auto_reply: true diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 8ef02401..770fccc7 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -29,16 +29,6 @@ "default": "./dist/index.cjs" } }, - "./client": { - "import": { - "types": "./dist/client.d.ts", - "default": "./dist/client.js" - }, - "require": { - "types": "./dist/client.d.cts", - "default": "./dist/client.cjs" - } - }, "./schema": { "import": { "types": "./dist/schema.d.ts", diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index 3b14d637..98574b86 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -416,15 +416,24 @@ export abstract class BaseCrudDialect { return this.buildEnumFilter(eb, modelAlias, field, fieldDef, payload); } - return match(fieldDef.type as BuiltinType) - .with('String', () => this.buildStringFilter(eb, modelAlias, field, payload)) - .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.buildNumberFilter(eb, model, modelAlias, field, type, payload), - ) - .with('Boolean', () => this.buildBooleanFilter(eb, modelAlias, field, payload)) - .with('DateTime', () => this.buildDateTimeFilter(eb, modelAlias, field, payload)) - .with('Bytes', () => this.buildBytesFilter(eb, modelAlias, field, payload)) - .exhaustive(); + return ( + match(fieldDef.type as BuiltinType) + .with('String', () => this.buildStringFilter(eb, modelAlias, field, payload)) + .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => + this.buildNumberFilter(eb, model, modelAlias, field, type, payload), + ) + .with('Boolean', () => this.buildBooleanFilter(eb, modelAlias, field, payload)) + .with('DateTime', () => this.buildDateTimeFilter(eb, modelAlias, field, payload)) + .with('Bytes', () => this.buildBytesFilter(eb, modelAlias, field, payload)) + // TODO: JSON filters + .with('Json', () => { + throw new InternalError('JSON filters are not supported yet'); + }) + .with('Unsupported', () => { + throw new QueryError(`Unsupported field cannot be used in filters`); + }) + .exhaustive() + ); } private buildLiteralFilter(eb: ExpressionBuilder, lhs: Expression, type: BuiltinType, rhs: unknown) { diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index 63573819..efe09a35 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -37,6 +37,7 @@ export class PostgresCrudDialect extends BaseCrudDiale .with('DateTime', () => value instanceof Date ? value : typeof value === 'string' ? new Date(value) : value, ) + .with('Decimal', () => (value !== null ? value.toString() : value)) .otherwise(() => value); } } diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index c8f20173..2d919745 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -39,6 +39,7 @@ export class SqliteCrudDialect extends BaseCrudDialect .with('DateTime', () => (value instanceof Date ? value.toISOString() : value)) .with('Decimal', () => (value as Decimal).toString()) .with('Bytes', () => Buffer.from(value as Uint8Array)) + .with('Json', () => JSON.stringify(value)) .otherwise(() => value); } } diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index cfda876d..46a7d7d1 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -379,15 +379,20 @@ export class InputValidator { } private makePrimitiveFilterSchema(type: BuiltinType, optional: boolean) { - return match(type) - .with('String', () => this.makeStringFilterSchema(optional)) - .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => - this.makeNumberFilterSchema(this.makePrimitiveSchema(type), optional), - ) - .with('Boolean', () => this.makeBooleanFilterSchema(optional)) - .with('DateTime', () => this.makeDateTimeFilterSchema(optional)) - .with('Bytes', () => this.makeBytesFilterSchema(optional)) - .exhaustive(); + return ( + match(type) + .with('String', () => this.makeStringFilterSchema(optional)) + .with(P.union('Int', 'Float', 'Decimal', 'BigInt'), (type) => + this.makeNumberFilterSchema(this.makePrimitiveSchema(type), optional), + ) + .with('Boolean', () => this.makeBooleanFilterSchema(optional)) + .with('DateTime', () => this.makeDateTimeFilterSchema(optional)) + .with('Bytes', () => this.makeBytesFilterSchema(optional)) + // TODO: JSON filters + .with('Json', () => z.any()) + .with('Unsupported', () => z.never()) + .exhaustive() + ); } private makeDateTimeFilterSchema(optional: boolean): ZodType { diff --git a/packages/runtime/src/client/helpers/schema-db-pusher.ts b/packages/runtime/src/client/helpers/schema-db-pusher.ts index 34974482..9bb5ba19 100644 --- a/packages/runtime/src/client/helpers/schema-db-pusher.ts +++ b/packages/runtime/src/client/helpers/schema-db-pusher.ts @@ -154,6 +154,7 @@ export class SchemaDbPusher { .with('Decimal', () => 'decimal') .with('DateTime', () => 'timestamp') .with('Bytes', () => (this.schema.provider.type === 'postgresql' ? 'bytea' : 'blob')) + .with('Json', () => 'jsonb') .otherwise(() => { throw new Error(`Unsupported field type: ${type}`); }); diff --git a/packages/runtime/src/client/result-processor.ts b/packages/runtime/src/client/result-processor.ts index 6a922c18..8c6e9df4 100644 --- a/packages/runtime/src/client/result-processor.ts +++ b/packages/runtime/src/client/result-processor.ts @@ -90,6 +90,7 @@ export class ResultProcessor { .with('Bytes', () => this.transformBytes(value)) .with('Decimal', () => this.transformDecimal(value)) .with('BigInt', () => this.transformBigInt(value)) + .with('Json', () => this.transformJson(value)) .otherwise(() => value); } @@ -156,4 +157,14 @@ export class ResultProcessor { } } } + + private transformJson(value: unknown) { + return match(this.schema.provider.type) + .with('sqlite', () => { + // better-sqlite3 returns JSON as string + invariant(typeof value === 'string', 'Expected string, got ' + typeof value); + return JSON.parse(value as string); + }) + .otherwise(() => value); + } } diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 2fd331c1..4f1cce44 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1 +1 @@ -export { ZenStackClient, type ClientContract } from './client'; +export * from './client'; diff --git a/packages/runtime/test/client-api/type-coverage.test.ts b/packages/runtime/test/client-api/type-coverage.test.ts index f525026b..81aa706d 100644 --- a/packages/runtime/test/client-api/type-coverage.test.ts +++ b/packages/runtime/test/client-api/type-coverage.test.ts @@ -2,10 +2,28 @@ import Decimal from 'decimal.js'; import { describe, expect, it } from 'vitest'; import { createTestClient } from '../utils'; -describe('zmodel type coverage tests', () => { - it('supports all types', async () => { - const db = await createTestClient( - ` +const PG_DB_NAME = 'client-api-type-coverage-tests'; + +describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', (provider) => { + it('supports all types - plain', async () => { + 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]), + Json: { foo: 'bar' }, + }; + + let db: any; + try { + db = await createTestClient( + ` model Foo { id String @id @default(cuid()) @@ -17,28 +35,67 @@ describe('zmodel type coverage tests', () => { Decimal Decimal Boolean Boolean Bytes Bytes + Json Json @@allow('all', true) } `, - ); + { provider, dbName: PG_DB_NAME }, + ); + + await db.foo.create({ data }); + await expect(db.foo.findUnique({ where: { id: '1' } })).resolves.toMatchObject(data); + } finally { + await db?.$disconnect(); + } + }); + + it('supports all types - array', async () => { + if (provider === 'sqlite') { + return; + } 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]), + 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])], + Json: [{ foo: 'bar' }], }; - await db.foo.create({ data }); + let db: any; + try { + db = await createTestClient( + ` + model Foo { + id String @id @default(cuid()) + + String String[] + Int Int[] + BigInt BigInt[] + DateTime DateTime[] + Float Float[] + Decimal Decimal[] + Boolean Boolean[] + Bytes Bytes[] + Json Json[] + + @@allow('all', true) + } + `, + { provider, dbName: PG_DB_NAME }, + ); - const r = await db.foo.findUnique({ where: { id: '1' } }); - expect(r.Bytes).toEqual(data.Bytes); + await db.foo.create({ data }); + await expect(db.foo.findUnique({ where: { id: '1' } })).resolves.toMatchObject(data); + } finally { + await db?.$disconnect(); + } }); }); diff --git a/packages/runtime/tsconfig.test.json b/packages/runtime/tsconfig.test.json index faef15af..014b54e4 100644 --- a/packages/runtime/tsconfig.test.json +++ b/packages/runtime/tsconfig.test.json @@ -4,6 +4,5 @@ "noEmit": true, "noImplicitAny": false }, - "include": ["test/**/*.ts"] } diff --git a/packages/runtime/tsup.config.ts b/packages/runtime/tsup.config.ts index 114183c4..0f8a9d6f 100644 --- a/packages/runtime/tsup.config.ts +++ b/packages/runtime/tsup.config.ts @@ -3,7 +3,6 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: { index: 'src/index.ts', - client: 'src/client/index.ts', schema: 'src/schema/index.ts', 'plugins/policy': 'src/plugins/policy/index.ts', }, diff --git a/packages/sdk/src/schema/schema.ts b/packages/sdk/src/schema/schema.ts index 8ef02fb6..91d333b5 100644 --- a/packages/sdk/src/schema/schema.ts +++ b/packages/sdk/src/schema/schema.ts @@ -73,7 +73,17 @@ export type ProcedureDef = { mutation?: boolean; }; -export type BuiltinType = 'String' | 'Boolean' | 'Int' | 'Float' | 'BigInt' | 'Decimal' | 'DateTime' | 'Bytes'; +export type BuiltinType = + | 'String' + | 'Boolean' + | 'Int' + | 'Float' + | 'BigInt' + | 'Decimal' + | 'DateTime' + | 'Bytes' + | 'Json' + | 'Unsupported'; export type MappedBuiltinType = string | boolean | number | bigint | Decimal | Date; diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index c1cc020b..79c1ec4c 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -695,7 +695,8 @@ export class TsSchemaGenerator { ? ts.factory.createStringLiteral(field.type.type) : field.type.reference ? ts.factory.createStringLiteral(field.type.reference.$refText) - : ts.factory.createStringLiteral('unknown'); + : // `Unsupported` type + ts.factory.createStringLiteral('Unsupported'); } private createEnumObject(e: Enum) { diff --git a/packages/tanstack-query/src/react.ts b/packages/tanstack-query/src/react.ts index 2b9bb819..c87115d5 100644 --- a/packages/tanstack-query/src/react.ts +++ b/packages/tanstack-query/src/react.ts @@ -5,7 +5,7 @@ import type { UseQueryOptions, UseQueryResult, } from '@tanstack/react-query'; -import type { CreateArgs, FindArgs, ModelResult, SelectSubset } from '@zenstackhq/runtime/client'; +import type { CreateArgs, FindArgs, ModelResult, SelectSubset } from '@zenstackhq/runtime'; import type { GetModels, SchemaDef } from '@zenstackhq/runtime/schema'; export type toHooks = {